git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #688 from oauth2-proxy/session-middlewares

Refactor session loading to make use of middleware pattern

commitf141f7cea0c6cc9993b0613f2ff3d8025683ee8c authorJoel Speed <Joel.speed@hotmail.co.uk> dateSun 19 Jul 2020, 20:40 (6 years ago) parent1aac37d3f00143
17 files changed+1940−309
CHANGELOG.md+1 oauthproxy.go+49−173 oauthproxy_test.go+1−65 pkg/apis/middleware/scope.go+24 pkg/apis/middleware/session.go+24 pkg/middleware/basic_session.go+88 pkg/middleware/basic_session_test.go+132 pkg/middleware/jwt_session.go+168 pkg/middleware/jwt_session_test.go+492 pkg/middleware/scope.go+39 pkg/middleware/scope_test.go+94 pkg/middleware/session_utils.go+33 pkg/middleware/session_utils_test.go+103 pkg/middleware/stored_session.go+165 pkg/middleware/stored_session_test.go+524 providers/provider_default.go+3−30 providers/provider_default_test.go−41
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111
1212## Changes since v6.0.0
1313
14+- [#688](https://github.com/oauth2-proxy/oauth2-proxy/pull/688) Refactor session loading to make use of middleware pattern (@JoelSpeed)
1415- [#593](https://github.com/oauth2-proxy/oauth2-proxy/pull/593) Integrate upstream package with OAuth2 Proxy (@JoelSpeed)
1516- [#687](https://github.com/oauth2-proxy/oauth2-proxy/pull/687) Refactor HTPasswd Validator (@JoelSpeed)
1617- [#624](https://github.com/oauth2-proxy/oauth2-proxy/pull/624) Allow stripping authentication headers from whitelisted requests with `--skip-auth-strip-headers` (@NickMeves)
oauthproxy.go+49−173
@@ -15,7 +15,9 @@ import (
1515 "time"
1616
1717 "github.com/coreos/go-oidc"
18+ "github.com/justinas/alice"
1819 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
20+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
1921 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
2022 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
2123 "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
@@ -23,6 +25,7 @@ import (
2325 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
2426 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
2527 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
28+ "github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
2629 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
2730 "github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
2831 "github.com/oauth2-proxy/oauth2-proxy/providers"
@@ -98,6 +101,8 @@ type OAuthProxy struct {
98101 trustedIPs *ip.NetSet
99102 Banner string
100103 Footer string
104+
105+ sessionChain alice.Chain
101106}
102107
103108// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -156,6 +161,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
156161 }
157162 }
158163
164+ sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
165+
159166 return &OAuthProxy{
160167 CookieName: opts.Cookie.Name,
161168 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
@@ -209,9 +216,45 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
209216
210217 basicAuthValidator: basicAuthValidator,
211218 displayHtpasswdForm: basicAuthValidator != nil,
219+ sessionChain: sessionChain,
212220 }, nil
213221}
214222
223+func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
224+ chain := alice.New(middleware.NewScope())
225+
226+ if opts.SkipJwtBearerTokens {
227+ sessionLoaders := []middlewareapi.TokenToSessionLoader{}
228+ if opts.GetOIDCVerifier() != nil {
229+ sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
230+ Verifier: opts.GetOIDCVerifier(),
231+ TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
232+ })
233+ }
234+
235+ for _, verifier := range opts.GetJWTBearerVerifiers() {
236+ sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
237+ Verifier: verifier,
238+ })
239+ }
240+
241+ chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
242+ }
243+
244+ if validator != nil {
245+ chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
246+ }
247+
248+ chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
249+ SessionStore: sessionStore,
250+ RefreshPeriod: opts.Cookie.Refresh,
251+ RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
252+ ValidateSessionState: opts.GetProvider().ValidateSessionState,
253+ }))
254+
255+ return chain
256+}
257+
215258// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
216259// redirect clients to once authenticated
217260func (p *OAuthProxy) GetRedirectURI(host string) string {
@@ -780,86 +823,20 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
780823// Set-Cookie headers may be set on the response as a side-effect of calling this method.
781824func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
782825 var session *sessionsapi.SessionState
783− var err error
784− var saveSession, clearSession, revalidated bool
785826
786− if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
787− session, err = p.GetJwtSession(req)
788− if err != nil {
789− logger.Printf("Error retrieving session from token in Authorization header: %s", err)
790− }
791− if session != nil {
792− saveSession = false
793− }
794− }
827+ getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
828+ session = middleware.GetRequestScope(req).Session
829+ }))
830+ getSession.ServeHTTP(rw, req)
795831
796− remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
797832 if session == nil {
798− session, err = p.LoadCookiedSession(req)
799− if err != nil {
800− logger.Printf("Error loading cookied session: %s", err)
801− }
802−
803− if session != nil {
804− if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
805− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
806− saveSession = true
807− }
808−
809− if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
810− logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
811− clearSession = true
812− session = nil
813− } else if ok {
814− saveSession = true
815− revalidated = true
816− }
817− }
818− }
819−
820− if session != nil && session.IsExpired() {
821− logger.Printf("Removing session: token expired %s", session)
822− session = nil
823− saveSession = false
824− clearSession = true
825− }
826−
827− if saveSession && !revalidated && session != nil && session.AccessToken != "" {
828− if !p.provider.ValidateSessionState(req.Context(), session) {
829− logger.Printf("Removing session: error validating %s", session)
830− saveSession = false
831− session = nil
832− clearSession = true
833− }
833+ return nil, ErrNeedsLogin
834834 }
835835
836836 if session != nil && session.Email != "" && !p.Validator(session.Email) {
837837 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
838− session = nil
839− saveSession = false
840− clearSession = true
841− }
842−
843− if saveSession && session != nil {
844− err = p.SaveSession(rw, req, session)
845− if err != nil {
846− logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
847− return nil, err
848− }
849− }
850−
851− if clearSession {
838+ // Invalid session, clear it
852839 p.ClearSessionCookie(rw, req)
853− }
854−
855− if session == nil {
856− session, err = p.CheckBasicAuth(req)
857− if err != nil {
858− logger.Printf("Error during basic auth validation: %s", err)
859− }
860− }
861−
862− if session == nil {
863840 return nil, ErrNeedsLogin
864841 }
865842
@@ -997,36 +974,6 @@ func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
997974 }
998975}
999976
1000−// CheckBasicAuth checks the requests Authorization header for basic auth
1001−// credentials and authenticates these against the proxies HtpasswdFile
1002−func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1003− if p.basicAuthValidator == nil {
1004− return nil, nil
1005− }
1006− auth := req.Header.Get("Authorization")
1007− if auth == "" {
1008− return nil, nil
1009− }
1010− s := strings.SplitN(auth, " ", 2)
1011− if len(s) != 2 || s[0] != "Basic" {
1012− return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1013− }
1014− b, err := b64.StdEncoding.DecodeString(s[1])
1015− if err != nil {
1016− return nil, err
1017− }
1018− pair := strings.SplitN(string(b), ":", 2)
1019− if len(pair) != 2 {
1020− return nil, fmt.Errorf("invalid format %s", b)
1021− }
1022− if p.basicAuthValidator.Validate(pair[0], pair[1]) {
1023− logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1024− return &sessionsapi.SessionState{User: pair[0]}, nil
1025− }
1026− logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1027− return nil, nil
1028−}
1029−
1030977// isAjax checks if a request is an ajax request
1031978func isAjax(req *http.Request) bool {
1032979 acceptValues := req.Header.Values("Accept")
@@ -1044,74 +991,3 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1044991 rw.Header().Set("Content-Type", applicationJSON)
1045992 rw.WriteHeader(code)
1046993}
1047−
1048−// GetJwtSession loads a session based on a JWT token in the authorization header.
1049−// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1050−func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1051− rawBearerToken, err := p.findBearerToken(req)
1052− if err != nil {
1053− return nil, err
1054− }
1055−
1056− // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1057− // and Bearer Token -> Session converter
1058− if p.mainJwtBearerVerifier != nil {
1059− bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1060− if err == nil {
1061− return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1062− }
1063− }
1064−
1065− // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1066− // Bearer Token -> Session converter
1067− for _, verifier := range p.extraJwtBearerVerifiers {
1068− bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1069− if err != nil {
1070− continue
1071− }
1072−
1073− return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1074− }
1075− return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1076−}
1077−
1078−// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1079−func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1080− auth := req.Header.Get("Authorization")
1081− s := strings.SplitN(auth, " ", 2)
1082− if len(s) != 2 {
1083− return "", fmt.Errorf("invalid authorization header %s", auth)
1084− }
1085− jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1086− var rawBearerToken string
1087− if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1088− rawBearerToken = s[1]
1089− } else if s[0] == "Basic" {
1090− // Check if we have a Bearer token masquerading in Basic
1091− b, err := b64.StdEncoding.DecodeString(s[1])
1092− if err != nil {
1093− return "", err
1094− }
1095− pair := strings.SplitN(string(b), ":", 2)
1096− if len(pair) != 2 {
1097− return "", fmt.Errorf("invalid format %s", b)
1098− }
1099− user, password := pair[0], pair[1]
1100−
1101− // check user, user+password, or just password for a token
1102− if jwtRegex.MatchString(user) {
1103− // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1104− if password == "" || password == "x-oauth-basic" {
1105− rawBearerToken = user
1106− }
1107− } else if jwtRegex.MatchString(password) {
1108− // support passwords and ignore user
1109− rawBearerToken = password
1110− }
1111− }
1112− if rawBearerToken == "" {
1113− return "", fmt.Errorf("no valid bearer token found in authorization header")
1114− }
1115−
1116− return rawBearerToken, nil
1117−}
oauthproxy_test.go+1−65
@@ -1889,7 +1889,7 @@ func TestGetJwtSession(t *testing.T) {
18891889
18901890 // Bearer
18911891 expires := time.Unix(1912151821, 0)
1892− session, err := test.proxy.GetJwtSession(test.req)
1892+ session, err := test.proxy.getAuthenticatedSession(test.rw, test.req)
18931893 assert.NoError(t, err)
18941894 assert.Equal(t, session.User, "1234567890")
18951895 assert.Equal(t, session.Email, "john@example.com")
@@ -1912,70 +1912,6 @@ func TestGetJwtSession(t *testing.T) {
19121912 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
19131913}
19141914
1915−func TestFindJwtBearerToken(t *testing.T) {
1916− p := OAuthProxy{CookieName: "oauth2", CookieDomains: []string{"abc"}}
1917− getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
1918−
1919− validToken := "eyJfoobar.eyJfoobar.12345asdf"
1920− var token string
1921−
1922− // Bearer
1923− getReq.Header = map[string][]string{
1924− "Authorization": {fmt.Sprintf("Bearer %s", validToken)},
1925− }
1926−
1927− token, err := p.findBearerToken(getReq)
1928− assert.NoError(t, err)
1929− assert.Equal(t, validToken, token)
1930−
1931− // Basic - no password
1932− getReq.SetBasicAuth(token, "")
1933− token, err = p.findBearerToken(getReq)
1934− assert.NoError(t, err)
1935− assert.Equal(t, validToken, token)
1936−
1937− // Basic - sentinel password
1938− getReq.SetBasicAuth(token, "x-oauth-basic")
1939− token, err = p.findBearerToken(getReq)
1940− assert.NoError(t, err)
1941− assert.Equal(t, validToken, token)
1942−
1943− // Basic - any username, password matching jwt pattern
1944− getReq.SetBasicAuth("any-username-you-could-wish-for", token)
1945− token, err = p.findBearerToken(getReq)
1946− assert.NoError(t, err)
1947− assert.Equal(t, validToken, token)
1948−
1949− failures := []string{
1950− // Too many parts
1951− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1952− // Not enough parts
1953− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
1954− // Invalid encrypted key
1955− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
1956− // Invalid IV
1957− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
1958− // Invalid ciphertext
1959− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
1960− // Invalid tag
1961− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
1962− // Invalid header
1963− "W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1964− // Invalid header
1965− "######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1966− // Missing alc/enc params
1967− "e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1968− }
1969−
1970− for _, failure := range failures {
1971− getReq.Header = map[string][]string{
1972− "Authorization": {fmt.Sprintf("Bearer %s", failure)},
1973− }
1974− _, err := p.findBearerToken(getReq)
1975− assert.Error(t, err)
1976− }
1977−}
1978−
19791915func Test_prepareNoCache(t *testing.T) {
19801916 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19811917 prepareNoCache(w)
pkg/apis/middleware/scope.gonew file+24
@@ -0,0 +1,24 @@
1+package middleware
2+
3+import (
4+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
5+)
6+
7+// RequestScope contains information regarding the request that is being made.
8+// The RequestScope is used to pass information between different middlewares
9+// within the chain.
10+type RequestScope struct {
11+ // Session details the authenticated users information (if it exists).
12+ Session *sessions.SessionState
13+
14+ // SaveSession indicates whether the session storage should attempt to save
15+ // the session or not.
16+ SaveSession bool
17+
18+ // ClearSession indicates whether the user should be logged out or not.
19+ ClearSession bool
20+
21+ // SessionRevalidated indicates whether the session has been revalidated since
22+ // it was loaded or not.
23+ SessionRevalidated bool
24+}
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/basic_session.gonew file+88
@@ -0,0 +1,88 @@
1+package middleware
2+
3+import (
4+ "fmt"
5+ "net/http"
6+
7+ "github.com/justinas/alice"
8+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
10+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
11+)
12+
13+func NewBasicAuthSessionLoader(validator basic.Validator) alice.Constructor {
14+ return func(next http.Handler) http.Handler {
15+ return loadBasicAuthSession(validator, next)
16+ }
17+}
18+
19+// loadBasicAuthSession attmepts to load a session from basic auth credentials
20+// stored in an Authorization header within the request.
21+// If no authorization header is found, or the header is invalid, no session
22+// will be loaded and the request will be passed to the next handler.
23+// If a session was loaded by a previous handler, it will not be replaced.
24+func loadBasicAuthSession(validator basic.Validator, next http.Handler) http.Handler {
25+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
26+ scope := GetRequestScope(req)
27+ // If scope is nil, this will panic.
28+ // A scope should always be injected before this handler is called.
29+ if scope.Session != nil {
30+ // The session was already loaded, pass to the next handler
31+ next.ServeHTTP(rw, req)
32+ return
33+ }
34+
35+ session, err := getBasicSession(validator, req)
36+ if err != nil {
37+ logger.Printf("Error retrieving session from token in Authorization header: %v", err)
38+ }
39+
40+ // Add the session to the scope if it was found
41+ scope.Session = session
42+ next.ServeHTTP(rw, req)
43+ })
44+}
45+
46+// getBasicSession attempts to load a basic session from the request.
47+// If the credentials in the request exist within the htpasswdMap,
48+// a new session will be created.
49+func getBasicSession(validator basic.Validator, req *http.Request) (*sessionsapi.SessionState, error) {
50+ auth := req.Header.Get("Authorization")
51+ if auth == "" {
52+ // No auth header provided, so don't attempt to load a session
53+ return nil, nil
54+ }
55+
56+ user, password, err := findBasicCredentialsFromHeader(auth)
57+ if err != nil {
58+ return nil, err
59+ }
60+
61+ if validator.Validate(user, password) {
62+ logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
63+ return &sessionsapi.SessionState{User: user}, nil
64+ }
65+
66+ logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
67+ return nil, nil
68+}
69+
70+// findBasicCredentialsFromHeader finds basic auth credneitals from the
71+// Authorization header of a given request.
72+func findBasicCredentialsFromHeader(header string) (string, string, error) {
73+ tokenType, token, err := splitAuthHeader(header)
74+ if err != nil {
75+ return "", "", err
76+ }
77+
78+ if tokenType != "Basic" {
79+ return "", "", fmt.Errorf("invalid Authorization header: %q", header)
80+ }
81+
82+ user, password, err := getBasicAuthCredentials(token)
83+ if err != nil {
84+ return "", "", fmt.Errorf("error decoding basic auth credentials: %v", err)
85+ }
86+
87+ return user, password, nil
88+}
pkg/middleware/basic_session_test.gonew file+132
@@ -0,0 +1,132 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+
9+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
10+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/ginkgo/extensions/table"
13+ . "github.com/onsi/gomega"
14+)
15+
16+const (
17+ adminUser = "admin"
18+ adminPassword = "Adm1n1str$t0r"
19+ user1 = "user1"
20+ user1Password = "UsErOn3P455"
21+ user2 = "user2"
22+ user2Password = "us3r2P455W0Rd!"
23+)
24+
25+var _ = Describe("Basic Auth Session Suite", func() {
26+ Context("BasicAuthSessionLoader", func() {
27+
28+ type basicAuthSessionLoaderTableInput struct {
29+ authorizationHeader string
30+ existingSession *sessionsapi.SessionState
31+ expectedSession *sessionsapi.SessionState
32+ }
33+
34+ DescribeTable("with an authorization header",
35+ func(in basicAuthSessionLoaderTableInput) {
36+ scope := &middlewareapi.RequestScope{
37+ Session: in.existingSession,
38+ }
39+
40+ // Set up the request with the authorization header and a request scope
41+ req := httptest.NewRequest("", "/", nil)
42+ req.Header.Set("Authorization", in.authorizationHeader)
43+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
44+ req = req.WithContext(contextWithScope)
45+
46+ rw := httptest.NewRecorder()
47+
48+ validator := fakeBasicValidator{
49+ users: map[string]string{
50+ adminUser: adminPassword,
51+ user1: user1Password,
52+ user2: user2Password,
53+ },
54+ }
55+
56+ // Create the handler with a next handler that will capture the session
57+ // from the scope
58+ var gotSession *sessionsapi.SessionState
59+ handler := NewBasicAuthSessionLoader(validator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60+ gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
61+ }))
62+ handler.ServeHTTP(rw, req)
63+
64+ Expect(gotSession).To(Equal(in.expectedSession))
65+ },
66+ Entry("<no value>", basicAuthSessionLoaderTableInput{
67+ authorizationHeader: "",
68+ existingSession: nil,
69+ expectedSession: nil,
70+ }),
71+ Entry("abcdef", basicAuthSessionLoaderTableInput{
72+ authorizationHeader: "abcdef",
73+ existingSession: nil,
74+ expectedSession: nil,
75+ }),
76+ Entry("abcdef (with existing session)", basicAuthSessionLoaderTableInput{
77+ authorizationHeader: "abcdef",
78+ existingSession: &sessionsapi.SessionState{User: "user"},
79+ expectedSession: &sessionsapi.SessionState{User: "user"},
80+ }),
81+ Entry("Bearer <password>", basicAuthSessionLoaderTableInput{
82+ authorizationHeader: fmt.Sprintf("Bearer %s", adminPassword),
83+ existingSession: nil,
84+ expectedSession: nil,
85+ }),
86+ Entry("Basic <password>", basicAuthSessionLoaderTableInput{
87+ authorizationHeader: fmt.Sprintf("Basic %s", adminPassword),
88+ existingSession: nil,
89+ expectedSession: nil,
90+ }),
91+ Entry("Basic Base64(:<password>) (with existing session)", basicAuthSessionLoaderTableInput{
92+ authorizationHeader: "Basic OlVzRXJPbjNQNDU1",
93+ existingSession: &sessionsapi.SessionState{User: "user"},
94+ expectedSession: &sessionsapi.SessionState{User: "user"},
95+ }),
96+ Entry("Basic Base64(user1:<user1Password>)", basicAuthSessionLoaderTableInput{
97+ authorizationHeader: "Basic dXNlcjE6VXNFck9uM1A0NTU=",
98+ existingSession: nil,
99+ expectedSession: &sessionsapi.SessionState{User: "user1"},
100+ }),
101+ Entry("Basic Base64(user2:<user1Password>)", basicAuthSessionLoaderTableInput{
102+ authorizationHeader: "Basic dXNlcjI6VXNFck9uM1A0NTU=",
103+ existingSession: nil,
104+ expectedSession: nil,
105+ }),
106+ Entry("Basic Base64(user2:<user2Password>)", basicAuthSessionLoaderTableInput{
107+ authorizationHeader: "Basic dXNlcjI6dXMzcjJQNDU1VzBSZCE=",
108+ existingSession: nil,
109+ expectedSession: &sessionsapi.SessionState{User: "user2"},
110+ }),
111+ Entry("Basic Base64(admin:<adminPassword>)", basicAuthSessionLoaderTableInput{
112+ authorizationHeader: "Basic YWRtaW46QWRtMW4xc3RyJHQwcg==",
113+ existingSession: nil,
114+ expectedSession: &sessionsapi.SessionState{User: "admin"},
115+ }),
116+ )
117+ })
118+})
119+
120+type fakeBasicValidator struct {
121+ users map[string]string
122+}
123+
124+func (f fakeBasicValidator) Validate(user, password string) bool {
125+ if f.users == nil {
126+ return false
127+ }
128+ if realPassword, ok := f.users[user]; ok {
129+ return realPassword == password
130+ }
131+ return false
132+}
pkg/middleware/jwt_session.gonew file+168
@@ -0,0 +1,168 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "regexp"
8+
9+ "github.com/coreos/go-oidc"
10+ "github.com/justinas/alice"
11+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
12+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
13+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
14+)
15+
16+const jwtRegexFormat = `^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`
17+
18+func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionLoader) alice.Constructor {
19+ for i, loader := range sessionLoaders {
20+ if loader.TokenToSession == nil {
21+ sessionLoaders[i] = middlewareapi.TokenToSessionLoader{
22+ Verifier: loader.Verifier,
23+ TokenToSession: createSessionStateFromBearerToken,
24+ }
25+ }
26+ }
27+
28+ js := &jwtSessionLoader{
29+ jwtRegex: regexp.MustCompile(jwtRegexFormat),
30+ sessionLoaders: sessionLoaders,
31+ }
32+ return js.loadSession
33+}
34+
35+// jwtSessionLoader is responsible for loading sessions from JWTs in
36+// Authorization headers.
37+type jwtSessionLoader struct {
38+ jwtRegex *regexp.Regexp
39+ sessionLoaders []middlewareapi.TokenToSessionLoader
40+}
41+
42+// loadSession attempts to load a session from a JWT stored in an Authorization
43+// header within the request.
44+// If no authorization header is found, or the header is invalid, no session
45+// will be loaded and the request will be passed to the next handler.
46+// If a session was loaded by a previous handler, it will not be replaced.
47+func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
48+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
49+ scope := GetRequestScope(req)
50+ // If scope is nil, this will panic.
51+ // A scope should always be injected before this handler is called.
52+ if scope.Session != nil {
53+ // The session was already loaded, pass to the next handler
54+ next.ServeHTTP(rw, req)
55+ return
56+ }
57+
58+ session, err := j.getJwtSession(req)
59+ if err != nil {
60+ logger.Printf("Error retrieving session from token in Authorization header: %v", err)
61+ }
62+
63+ // Add the session to the scope if it was found
64+ scope.Session = session
65+ next.ServeHTTP(rw, req)
66+ })
67+}
68+
69+// getJwtSession loads a session based on a JWT token in the authorization header.
70+// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
71+func (j *jwtSessionLoader) getJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
72+ auth := req.Header.Get("Authorization")
73+ if auth == "" {
74+ // No auth header provided, so don't attempt to load a session
75+ return nil, nil
76+ }
77+
78+ rawBearerToken, err := j.findBearerTokenFromHeader(auth)
79+ if err != nil {
80+ return nil, err
81+ }
82+
83+ for _, loader := range j.sessionLoaders {
84+ bearerToken, err := loader.Verifier.Verify(req.Context(), rawBearerToken)
85+ if err == nil {
86+ // The token was verified, convert it to a session
87+ return loader.TokenToSession(req.Context(), rawBearerToken, bearerToken)
88+ }
89+ }
90+
91+ return nil, fmt.Errorf("unable to verify jwt token: %q", req.Header.Get("Authorization"))
92+}
93+
94+// findBearerTokenFromHeader finds a valid JWT token from the Authorization header of a given request.
95+func (j *jwtSessionLoader) findBearerTokenFromHeader(header string) (string, error) {
96+ tokenType, token, err := splitAuthHeader(header)
97+ if err != nil {
98+ return "", err
99+ }
100+
101+ if tokenType == "Bearer" && j.jwtRegex.MatchString(token) {
102+ // Found a JWT as a bearer token
103+ return token, nil
104+ }
105+
106+ if tokenType == "Basic" {
107+ // Check if we have a Bearer token masquerading in Basic
108+ return j.getBasicToken(token)
109+ }
110+
111+ return "", fmt.Errorf("no valid bearer token found in authorization header")
112+}
113+
114+// getBasicToken tries to extract a token from the basic value provided.
115+func (j *jwtSessionLoader) getBasicToken(token string) (string, error) {
116+ user, password, err := getBasicAuthCredentials(token)
117+ if err != nil {
118+ return "", err
119+ }
120+
121+ // check user, user+password, or just password for a token
122+ if j.jwtRegex.MatchString(user) {
123+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
124+ if password == "" || password == "x-oauth-basic" {
125+ return user, nil
126+ }
127+ } else if j.jwtRegex.MatchString(password) {
128+ // support passwords and ignore user
129+ return password, nil
130+ }
131+
132+ return "", fmt.Errorf("invalid basic auth token found in authorization header")
133+}
134+
135+// createSessionStateFromBearerToken is a default implementation for converting
136+// a JWT into a session state.
137+func createSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessionsapi.SessionState, error) {
138+ var claims struct {
139+ Subject string `json:"sub"`
140+ Email string `json:"email"`
141+ Verified *bool `json:"email_verified"`
142+ PreferredUsername string `json:"preferred_username"`
143+ }
144+
145+ if err := idToken.Claims(&claims); err != nil {
146+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
147+ }
148+
149+ if claims.Email == "" {
150+ claims.Email = claims.Subject
151+ }
152+
153+ if claims.Verified != nil && !*claims.Verified {
154+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
155+ }
156+
157+ newSession := &sessionsapi.SessionState{
158+ Email: claims.Email,
159+ User: claims.Subject,
160+ PreferredUsername: claims.PreferredUsername,
161+ AccessToken: rawIDToken,
162+ IDToken: rawIDToken,
163+ RefreshToken: "",
164+ ExpiresOn: &idToken.Expiry,
165+ }
166+
167+ return newSession, nil
168+}
pkg/middleware/jwt_session_test.gonew file+492
@@ -0,0 +1,492 @@
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("createSessionStateFromBearerToken", func() {
385+ ctx := context.Background()
386+ expiresFuture := time.Now().Add(time.Duration(5) * time.Minute)
387+ verified := true
388+ notVerified := false
389+
390+ type idTokenClaims struct {
391+ Email string `json:"email,omitempty"`
392+ Verified *bool `json:"email_verified,omitempty"`
393+ jwt.StandardClaims
394+ }
395+
396+ type createSessionStateTableInput struct {
397+ idToken idTokenClaims
398+ expectedErr error
399+ expectedUser string
400+ expectedEmail string
401+ expectedExpires *time.Time
402+ }
403+
404+ DescribeTable("when creating a session from an IDToken",
405+ func(in createSessionStateTableInput) {
406+ verifier := oidc.NewVerifier(
407+ "https://issuer.example.com",
408+ noOpKeySet{},
409+ &oidc.Config{ClientID: "asdf1234"},
410+ )
411+
412+ key, err := rsa.GenerateKey(rand.Reader, 2048)
413+ Expect(err).ToNot(HaveOccurred())
414+
415+ rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, in.idToken).SignedString(key)
416+ Expect(err).ToNot(HaveOccurred())
417+
418+ // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
419+ idToken, err := verifier.Verify(context.Background(), rawIDToken)
420+ Expect(err).ToNot(HaveOccurred())
421+
422+ session, err := createSessionStateFromBearerToken(ctx, rawIDToken, idToken)
423+ if in.expectedErr != nil {
424+ Expect(err).To(MatchError(in.expectedErr))
425+ Expect(session).To(BeNil())
426+ return
427+ }
428+
429+ Expect(err).ToNot(HaveOccurred())
430+ Expect(session.AccessToken).To(Equal(rawIDToken))
431+ Expect(session.IDToken).To(Equal(rawIDToken))
432+ Expect(session.User).To(Equal(in.expectedUser))
433+ Expect(session.Email).To(Equal(in.expectedEmail))
434+ Expect(session.ExpiresOn.Unix()).To(Equal(in.expectedExpires.Unix()))
435+ Expect(session.RefreshToken).To(BeEmpty())
436+ Expect(session.PreferredUsername).To(BeEmpty())
437+ },
438+ Entry("with no email", createSessionStateTableInput{
439+ idToken: idTokenClaims{
440+ StandardClaims: jwt.StandardClaims{
441+ Audience: "asdf1234",
442+ ExpiresAt: expiresFuture.Unix(),
443+ Id: "id-some-id",
444+ IssuedAt: time.Now().Unix(),
445+ Issuer: "https://issuer.example.com",
446+ NotBefore: 0,
447+ Subject: "123456789",
448+ },
449+ },
450+ expectedErr: nil,
451+ expectedUser: "123456789",
452+ expectedEmail: "123456789",
453+ expectedExpires: &expiresFuture,
454+ }),
455+ Entry("with a verified email", createSessionStateTableInput{
456+ idToken: idTokenClaims{
457+ StandardClaims: jwt.StandardClaims{
458+ Audience: "asdf1234",
459+ ExpiresAt: expiresFuture.Unix(),
460+ Id: "id-some-id",
461+ IssuedAt: time.Now().Unix(),
462+ Issuer: "https://issuer.example.com",
463+ NotBefore: 0,
464+ Subject: "123456789",
465+ },
466+ Email: "foo@example.com",
467+ Verified: &verified,
468+ },
469+ expectedErr: nil,
470+ expectedUser: "123456789",
471+ expectedEmail: "foo@example.com",
472+ expectedExpires: &expiresFuture,
473+ }),
474+ Entry("with a non-verified email", createSessionStateTableInput{
475+ idToken: idTokenClaims{
476+ StandardClaims: jwt.StandardClaims{
477+ Audience: "asdf1234",
478+ ExpiresAt: expiresFuture.Unix(),
479+ Id: "id-some-id",
480+ IssuedAt: time.Now().Unix(),
481+ Issuer: "https://issuer.example.com",
482+ NotBefore: 0,
483+ Subject: "123456789",
484+ },
485+ Email: "foo@example.com",
486+ Verified: &notVerified,
487+ },
488+ expectedErr: errors.New("email in id_token (foo@example.com) isn't verified"),
489+ }),
490+ )
491+ })
492+})
pkg/middleware/scope.gonew file+39
@@ -0,0 +1,39 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "net/http"
6+
7+ "github.com/justinas/alice"
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
9+)
10+
11+type scopeKey string
12+
13+// requestScopeKey uses a typed string to reduce likelihood of clasing
14+// with other context keys
15+const requestScopeKey scopeKey = "request-scope"
16+
17+func NewScope() alice.Constructor {
18+ return addScope
19+}
20+
21+// addScope injects a new request scope into the request context.
22+func addScope(next http.Handler) http.Handler {
23+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
24+ scope := &middlewareapi.RequestScope{}
25+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
26+ requestWithScope := req.WithContext(contextWithScope)
27+ next.ServeHTTP(rw, requestWithScope)
28+ })
29+}
30+
31+// GetRequestScope returns the current request scope from the given request
32+func GetRequestScope(req *http.Request) *middlewareapi.RequestScope {
33+ scope := req.Context().Value(requestScopeKey)
34+ if scope == nil {
35+ return nil
36+ }
37+
38+ return scope.(*middlewareapi.RequestScope)
39+}
pkg/middleware/scope_test.gonew file+94
@@ -0,0 +1,94 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+
8+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/gomega"
11+)
12+
13+var _ = Describe("Scope Suite", func() {
14+ Context("NewScope", func() {
15+ var request, nextRequest *http.Request
16+ var rw http.ResponseWriter
17+
18+ BeforeEach(func() {
19+ var err error
20+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
21+ Expect(err).ToNot(HaveOccurred())
22+
23+ rw = httptest.NewRecorder()
24+
25+ handler := NewScope()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+ nextRequest = r
27+ w.WriteHeader(200)
28+ }))
29+ handler.ServeHTTP(rw, request)
30+ })
31+
32+ It("does not add a scope to the original request", func() {
33+ Expect(request.Context().Value(requestScopeKey)).To(BeNil())
34+ })
35+
36+ It("cannot load a scope from the original request using GetRequestScope", func() {
37+ Expect(GetRequestScope(request)).To(BeNil())
38+ })
39+
40+ It("adds a scope to the request for the next handler", func() {
41+ Expect(nextRequest.Context().Value(requestScopeKey)).ToNot(BeNil())
42+ })
43+
44+ It("can load a scope from the next handler's request using GetRequestScope", func() {
45+ Expect(GetRequestScope(nextRequest)).ToNot(BeNil())
46+ })
47+ })
48+
49+ Context("GetRequestScope", func() {
50+ var request *http.Request
51+
52+ BeforeEach(func() {
53+ var err error
54+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
55+ Expect(err).ToNot(HaveOccurred())
56+ })
57+
58+ Context("with a scope", func() {
59+ var scope *middlewareapi.RequestScope
60+
61+ BeforeEach(func() {
62+ scope = &middlewareapi.RequestScope{}
63+ contextWithScope := context.WithValue(request.Context(), requestScopeKey, scope)
64+ request = request.WithContext(contextWithScope)
65+ })
66+
67+ It("returns the scope", func() {
68+ s := GetRequestScope(request)
69+ Expect(s).ToNot(BeNil())
70+ Expect(s).To(Equal(scope))
71+ })
72+
73+ Context("if the scope is then modified", func() {
74+ BeforeEach(func() {
75+ Expect(scope.SaveSession).To(BeFalse())
76+ scope.SaveSession = true
77+ })
78+
79+ It("returns the updated session", func() {
80+ s := GetRequestScope(request)
81+ Expect(s).ToNot(BeNil())
82+ Expect(s).To(Equal(scope))
83+ Expect(s.SaveSession).To(BeTrue())
84+ })
85+ })
86+ })
87+
88+ Context("without a scope", func() {
89+ It("returns nil", func() {
90+ Expect(GetRequestScope(request)).To(BeNil())
91+ })
92+ })
93+ })
94+})
pkg/middleware/session_utils.gonew file+33
@@ -0,0 +1,33 @@
1+package middleware
2+
3+import (
4+ "encoding/base64"
5+ "fmt"
6+ "strings"
7+)
8+
9+// splitAuthHeader takes the auth header value and splits it into the token type
10+// and the token value.
11+func splitAuthHeader(header string) (string, string, error) {
12+ s := strings.Split(header, " ")
13+ if len(s) != 2 {
14+ return "", "", fmt.Errorf("invalid authorization header: %q", header)
15+ }
16+ return s[0], s[1], nil
17+}
18+
19+// getBasicAuthCredentials decodes a basic auth token and extracts the user
20+// and password pair.
21+func getBasicAuthCredentials(token string) (string, string, error) {
22+ b, err := base64.StdEncoding.DecodeString(token)
23+ if err != nil {
24+ return "", "", fmt.Errorf("invalid basic auth token: %v", err)
25+ }
26+
27+ pair := strings.SplitN(string(b), ":", 2)
28+ if len(pair) != 2 {
29+ return "", "", fmt.Errorf("invalid format: %q", b)
30+ }
31+ // user, password
32+ return pair[0], pair[1], nil
33+}
pkg/middleware/session_utils_test.gonew file+103
@@ -0,0 +1,103 @@
1+package middleware
2+
3+import (
4+ "errors"
5+
6+ . "github.com/onsi/ginkgo"
7+ . "github.com/onsi/ginkgo/extensions/table"
8+ . "github.com/onsi/gomega"
9+)
10+
11+var _ = Describe("Session utilities suite", func() {
12+ Context("splitAuthHeader", func() {
13+ type splitAuthTableInput struct {
14+ header string
15+ expectedErr error
16+ expectedTokenType string
17+ expectedTokenValue string
18+ }
19+
20+ DescribeTable("with a header value",
21+ func(in splitAuthTableInput) {
22+ tt, tv, err := splitAuthHeader(in.header)
23+ if in.expectedErr != nil {
24+ Expect(err).To(MatchError(in.expectedErr))
25+ } else {
26+ Expect(err).ToNot(HaveOccurred())
27+ }
28+ Expect(tt).To(Equal(in.expectedTokenType))
29+ Expect(tv).To(Equal(in.expectedTokenValue))
30+ },
31+ Entry("Bearer abcdef", splitAuthTableInput{
32+ header: "Bearer abcdef",
33+ expectedErr: nil,
34+ expectedTokenType: "Bearer",
35+ expectedTokenValue: "abcdef",
36+ }),
37+ Entry("Bearer", splitAuthTableInput{
38+ header: "Bearer",
39+ expectedErr: errors.New("invalid authorization header: \"Bearer\""),
40+ expectedTokenType: "",
41+ expectedTokenValue: "",
42+ }),
43+ Entry("Bearer abc def", splitAuthTableInput{
44+ header: "Bearer abc def",
45+ expectedErr: errors.New("invalid authorization header: \"Bearer abc def\""),
46+ expectedTokenType: "",
47+ expectedTokenValue: "",
48+ }),
49+ )
50+ })
51+
52+ Context("getBasicAuthCredentials", func() {
53+ type getBasicAuthCredentialsTableInput struct {
54+ token string
55+ expectedErr error
56+ expectedUser string
57+ expectedPassword string
58+ }
59+
60+ DescribeTable("from token",
61+ func(in getBasicAuthCredentialsTableInput) {
62+ user, password, err := getBasicAuthCredentials(in.token)
63+ if in.expectedErr != nil {
64+ Expect(err).To(MatchError(in.expectedErr))
65+ } else {
66+ Expect(err).ToNot(HaveOccurred())
67+ }
68+ Expect(user).To(Equal(in.expectedUser))
69+ Expect(password).To(Equal(in.expectedPassword))
70+ },
71+ Entry("<no value>", getBasicAuthCredentialsTableInput{
72+ token: "",
73+ expectedErr: errors.New("invalid format: \"\""),
74+ expectedUser: "",
75+ expectedPassword: "",
76+ }),
77+ Entry("invalid-base64", getBasicAuthCredentialsTableInput{
78+ token: "invalid-base64",
79+ expectedErr: errors.New("invalid basic auth token: illegal base64 data at input byte 7"),
80+ expectedUser: "",
81+ expectedPassword: "",
82+ }),
83+ Entry("Base64(some-user:some-password)", getBasicAuthCredentialsTableInput{
84+ token: "c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ=",
85+ expectedErr: nil,
86+ expectedUser: "some-user",
87+ expectedPassword: "some-password",
88+ }),
89+ Entry("Base64(no-password:)", getBasicAuthCredentialsTableInput{
90+ token: "bm8tcGFzc3dvcmQ6",
91+ expectedErr: nil,
92+ expectedUser: "no-password",
93+ expectedPassword: "",
94+ }),
95+ Entry("Base64(:no-user)", getBasicAuthCredentialsTableInput{
96+ token: "Om5vLXVzZXI=",
97+ expectedErr: nil,
98+ expectedUser: "",
99+ expectedPassword: "no-user",
100+ }),
101+ )
102+ })
103+})
pkg/middleware/stored_session.gonew file+165
@@ -0,0 +1,165 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "net/http"
8+ "time"
9+
10+ "github.com/justinas/alice"
11+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
12+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
13+)
14+
15+// StoredSessionLoaderOptions cotnains all of the requirements to construct
16+// a stored session loader.
17+// All options must be provided.
18+type StoredSessionLoaderOptions struct {
19+ // Session storage basckend
20+ SessionStore sessionsapi.SessionStore
21+
22+ // How often should sessions be refreshed
23+ RefreshPeriod time.Duration
24+
25+ // Provider based sesssion refreshing
26+ RefreshSessionIfNeeded func(context.Context, *sessionsapi.SessionState) (bool, error)
27+
28+ // Provider based session validation.
29+ // If the sesssion is older than `RefreshPeriod` but the provider doesn't
30+ // refresh it, we must re-validate using this validation.
31+ ValidateSessionState func(context.Context, *sessionsapi.SessionState) bool
32+}
33+
34+// NewStoredSessionLoader creates a new storedSessionLoader which loads
35+// sessions from the session store.
36+// If no session is found, the request will be passed to the nex handler.
37+// If a session was loader by a previous handler, it will not be replaced.
38+func NewStoredSessionLoader(opts *StoredSessionLoaderOptions) alice.Constructor {
39+ ss := &storedSessionLoader{
40+ store: opts.SessionStore,
41+ refreshPeriod: opts.RefreshPeriod,
42+ refreshSessionWithProviderIfNeeded: opts.RefreshSessionIfNeeded,
43+ validateSessionState: opts.ValidateSessionState,
44+ }
45+ return ss.loadSession
46+}
47+
48+// storedSessionLoader is responsible for loading sessions from cookie
49+// identified sessions in the session store.
50+type storedSessionLoader struct {
51+ store sessionsapi.SessionStore
52+ refreshPeriod time.Duration
53+ refreshSessionWithProviderIfNeeded func(context.Context, *sessionsapi.SessionState) (bool, error)
54+ validateSessionState func(context.Context, *sessionsapi.SessionState) bool
55+}
56+
57+// loadSession attempts to load a session as identified by the request cookies.
58+// If no session is found, the request will be passed to the nex handler.
59+// If a session was loader by a previous handler, it will not be replaced.
60+func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
61+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
62+ scope := GetRequestScope(req)
63+ // If scope is nil, this will panic.
64+ // A scope should always be injected before this handler is called.
65+ if scope.Session != nil {
66+ // The session was already loaded, pass to the next handler
67+ next.ServeHTTP(rw, req)
68+ return
69+ }
70+
71+ session, err := s.getValidatedSession(rw, req)
72+ if err != nil {
73+ // In the case when there was an error loading the session,
74+ // we should clear the session
75+ logger.Printf("Error loading cookied session: %v, removing session", err)
76+ s.store.Clear(rw, req)
77+ }
78+
79+ // Add the session to the scope if it was found
80+ scope.Session = session
81+ next.ServeHTTP(rw, req)
82+ })
83+}
84+
85+// getValidatedSession is responsible for loading a session and making sure
86+// that is is valid.
87+func (s *storedSessionLoader) getValidatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
88+ session, err := s.store.Load(req)
89+ if err != nil {
90+ return nil, err
91+ }
92+ if session == nil {
93+ // No session was found in the storage, nothing more to do
94+ return nil, nil
95+ }
96+
97+ err = s.refreshSessionIfNeeded(rw, req, session)
98+ if err != nil {
99+ return nil, fmt.Errorf("error refreshing access token for session (%s): %v", session, err)
100+ }
101+
102+ return session, nil
103+}
104+
105+// refreshSessionIfNeeded will attempt to refresh a session if the session
106+// is older than the refresh period.
107+// It is assumed that if the provider refreshes the session, the session is now
108+// valid.
109+// If the session requires refreshing but the provider does not refresh it,
110+// we must validate the session to ensure that the returned session is still
111+// valid.
112+func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {
113+ if s.refreshPeriod <= time.Duration(0) || session.Age() < s.refreshPeriod {
114+ // Refresh is disabled or the session is not old enough, do nothing
115+ return nil
116+ }
117+
118+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, s.refreshPeriod)
119+ refreshed, err := s.refreshSessionWithProvider(rw, req, session)
120+ if err != nil {
121+ return err
122+ }
123+
124+ if !refreshed {
125+ // Session wasn't refreshed, so make sure it's still valid
126+ return s.validateSession(req.Context(), session)
127+ }
128+ return nil
129+}
130+
131+// refreshSessionWithProvider attempts to refresh the sessinon with the provider
132+// and will save the session if it was updated.
133+func (s *storedSessionLoader) refreshSessionWithProvider(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) (bool, error) {
134+ refreshed, err := s.refreshSessionWithProviderIfNeeded(req.Context(), session)
135+ if err != nil {
136+ return false, fmt.Errorf("error refreshing access token: %v", err)
137+ }
138+
139+ if !refreshed {
140+ return false, nil
141+ }
142+
143+ // Because the session was refreshed, make sure to save it
144+ err = s.store.Save(rw, req, session)
145+ if err != nil {
146+ logger.PrintAuthf(session.Email, req, logger.AuthError, "error saving session: %v", err)
147+ return false, fmt.Errorf("error saving session: %v", err)
148+ }
149+ return true, nil
150+}
151+
152+// validateSession checks whether the session has expired and performs
153+// provider validation on the session.
154+// An error implies the session is not longer valid.
155+func (s *storedSessionLoader) validateSession(ctx context.Context, session *sessionsapi.SessionState) error {
156+ if session.IsExpired() {
157+ return errors.New("session is expired")
158+ }
159+
160+ if !s.validateSessionState(ctx, session) {
161+ return errors.New("session is invalid")
162+ }
163+
164+ return nil
165+}
pkg/middleware/stored_session_test.gonew file+524
@@ -0,0 +1,524 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "net/http"
8+ "net/http/httptest"
9+ "time"
10+
11+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
12+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
13+ . "github.com/onsi/ginkgo"
14+ . "github.com/onsi/ginkgo/extensions/table"
15+ . "github.com/onsi/gomega"
16+)
17+
18+var _ = Describe("Stored Session Suite", func() {
19+ const (
20+ refresh = "Refresh"
21+ noRefresh = "NoRefresh"
22+ )
23+
24+ var ctx = context.Background()
25+
26+ Context("StoredSessionLoader", func() {
27+ createdPast := time.Now().Add(-5 * time.Minute)
28+ createdFuture := time.Now().Add(5 * time.Minute)
29+
30+ var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
31+ switch ss.RefreshToken {
32+ case refresh:
33+ ss.RefreshToken = "Refreshed"
34+ return true, nil
35+ case noRefresh:
36+ return false, nil
37+ default:
38+ return false, errors.New("error refreshing session")
39+ }
40+ }
41+
42+ var defaultValidateFunc = func(_ context.Context, ss *sessionsapi.SessionState) bool {
43+ return ss.AccessToken != "Invalid"
44+ }
45+
46+ var defaultSessionStore = &fakeSessionStore{
47+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
48+ switch req.Header.Get("Cookie") {
49+ case "_oauth2_proxy=NoRefreshSession":
50+ return &sessionsapi.SessionState{
51+ RefreshToken: noRefresh,
52+ CreatedAt: &createdPast,
53+ ExpiresOn: &createdFuture,
54+ }, nil
55+ case "_oauth2_proxy=InvalidNoRefreshSession":
56+ return &sessionsapi.SessionState{
57+ AccessToken: "Invalid",
58+ RefreshToken: noRefresh,
59+ CreatedAt: &createdPast,
60+ ExpiresOn: &createdFuture,
61+ }, nil
62+ case "_oauth2_proxy=ExpiredNoRefreshSession":
63+ return &sessionsapi.SessionState{
64+ RefreshToken: noRefresh,
65+ CreatedAt: &createdPast,
66+ ExpiresOn: &createdPast,
67+ }, nil
68+ case "_oauth2_proxy=RefreshSession":
69+ return &sessionsapi.SessionState{
70+ RefreshToken: refresh,
71+ CreatedAt: &createdPast,
72+ ExpiresOn: &createdFuture,
73+ }, nil
74+ case "_oauth2_proxy=RefreshError":
75+ return &sessionsapi.SessionState{
76+ RefreshToken: "RefreshError",
77+ CreatedAt: &createdPast,
78+ ExpiresOn: &createdFuture,
79+ }, nil
80+ case "_oauth2_proxy=NonExistent":
81+ return nil, fmt.Errorf("invalid cookie")
82+ default:
83+ return nil, nil
84+ }
85+ },
86+ }
87+
88+ type storedSessionLoaderTableInput struct {
89+ requestHeaders http.Header
90+ existingSession *sessionsapi.SessionState
91+ expectedSession *sessionsapi.SessionState
92+ store sessionsapi.SessionStore
93+ refreshPeriod time.Duration
94+ refreshSession func(context.Context, *sessionsapi.SessionState) (bool, error)
95+ validateSession func(context.Context, *sessionsapi.SessionState) bool
96+ }
97+
98+ DescribeTable("when serving a request",
99+ func(in storedSessionLoaderTableInput) {
100+ scope := &middlewareapi.RequestScope{
101+ Session: in.existingSession,
102+ }
103+
104+ // Set up the request with the request headesr and a request scope
105+ req := httptest.NewRequest("", "/", nil)
106+ req.Header = in.requestHeaders
107+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
108+ req = req.WithContext(contextWithScope)
109+
110+ rw := httptest.NewRecorder()
111+
112+ opts := &StoredSessionLoaderOptions{
113+ SessionStore: in.store,
114+ RefreshPeriod: in.refreshPeriod,
115+ RefreshSessionIfNeeded: in.refreshSession,
116+ ValidateSessionState: in.validateSession,
117+ }
118+
119+ // Create the handler with a next handler that will capture the session
120+ // from the scope
121+ var gotSession *sessionsapi.SessionState
122+ handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123+ gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
124+ }))
125+ handler.ServeHTTP(rw, req)
126+
127+ Expect(gotSession).To(Equal(in.expectedSession))
128+ },
129+ Entry("with no cookie", storedSessionLoaderTableInput{
130+ requestHeaders: http.Header{},
131+ existingSession: nil,
132+ expectedSession: nil,
133+ store: defaultSessionStore,
134+ refreshPeriod: 1 * time.Minute,
135+ refreshSession: defaultRefreshFunc,
136+ validateSession: defaultValidateFunc,
137+ }),
138+ Entry("with an invalid cookie", storedSessionLoaderTableInput{
139+ requestHeaders: http.Header{
140+ "Cookie": []string{"_oauth2_proxy=NonExistent"},
141+ },
142+ existingSession: nil,
143+ expectedSession: nil,
144+ store: defaultSessionStore,
145+ refreshPeriod: 1 * time.Minute,
146+ refreshSession: defaultRefreshFunc,
147+ validateSession: defaultValidateFunc,
148+ }),
149+ Entry("with an existing session", storedSessionLoaderTableInput{
150+ requestHeaders: http.Header{
151+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
152+ },
153+ existingSession: &sessionsapi.SessionState{
154+ RefreshToken: "Existing",
155+ },
156+ expectedSession: &sessionsapi.SessionState{
157+ RefreshToken: "Existing",
158+ },
159+ store: defaultSessionStore,
160+ refreshPeriod: 1 * time.Minute,
161+ refreshSession: defaultRefreshFunc,
162+ validateSession: defaultValidateFunc,
163+ }),
164+ Entry("with a session that has not expired", storedSessionLoaderTableInput{
165+ requestHeaders: http.Header{
166+ "Cookie": []string{"_oauth2_proxy=NoRefreshSession"},
167+ },
168+ existingSession: nil,
169+ expectedSession: &sessionsapi.SessionState{
170+ RefreshToken: noRefresh,
171+ CreatedAt: &createdPast,
172+ ExpiresOn: &createdFuture,
173+ },
174+ store: defaultSessionStore,
175+ refreshPeriod: 1 * time.Minute,
176+ refreshSession: defaultRefreshFunc,
177+ validateSession: defaultValidateFunc,
178+ }),
179+ Entry("with a session that cannot refresh and has expired", storedSessionLoaderTableInput{
180+ requestHeaders: http.Header{
181+ "Cookie": []string{"_oauth2_proxy=ExpiredNoRefreshSession"},
182+ },
183+ existingSession: nil,
184+ expectedSession: nil,
185+ store: defaultSessionStore,
186+ refreshPeriod: 1 * time.Minute,
187+ refreshSession: defaultRefreshFunc,
188+ validateSession: defaultValidateFunc,
189+ }),
190+ Entry("with a session that can refresh, but is younger than refresh period", storedSessionLoaderTableInput{
191+ requestHeaders: http.Header{
192+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
193+ },
194+ existingSession: nil,
195+ expectedSession: &sessionsapi.SessionState{
196+ RefreshToken: refresh,
197+ CreatedAt: &createdPast,
198+ ExpiresOn: &createdFuture,
199+ },
200+ store: defaultSessionStore,
201+ refreshPeriod: 10 * time.Minute,
202+ refreshSession: defaultRefreshFunc,
203+ validateSession: defaultValidateFunc,
204+ }),
205+ Entry("with a session that can refresh and is older than the refresh period", storedSessionLoaderTableInput{
206+ requestHeaders: http.Header{
207+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
208+ },
209+ existingSession: nil,
210+ expectedSession: &sessionsapi.SessionState{
211+ RefreshToken: "Refreshed",
212+ CreatedAt: &createdPast,
213+ ExpiresOn: &createdFuture,
214+ },
215+ store: defaultSessionStore,
216+ refreshPeriod: 1 * time.Minute,
217+ refreshSession: defaultRefreshFunc,
218+ validateSession: defaultValidateFunc,
219+ }),
220+ Entry("when the provider refresh fails", storedSessionLoaderTableInput{
221+ requestHeaders: http.Header{
222+ "Cookie": []string{"_oauth2_proxy=RefreshError"},
223+ },
224+ existingSession: nil,
225+ expectedSession: nil,
226+ store: defaultSessionStore,
227+ refreshPeriod: 1 * time.Minute,
228+ refreshSession: defaultRefreshFunc,
229+ validateSession: defaultValidateFunc,
230+ }),
231+ Entry("when the session is not refreshed and is no longer valid", storedSessionLoaderTableInput{
232+ requestHeaders: http.Header{
233+ "Cookie": []string{"_oauth2_proxy=InvalidNoRefreshSession"},
234+ },
235+ existingSession: nil,
236+ expectedSession: nil,
237+ store: defaultSessionStore,
238+ refreshPeriod: 1 * time.Minute,
239+ refreshSession: defaultRefreshFunc,
240+ validateSession: defaultValidateFunc,
241+ }),
242+ )
243+ })
244+
245+ Context("refreshSessionIfNeeded", func() {
246+ type refreshSessionIfNeededTableInput struct {
247+ refreshPeriod time.Duration
248+ session *sessionsapi.SessionState
249+ expectedErr error
250+ expectRefreshed bool
251+ expectValidated bool
252+ }
253+
254+ createdPast := time.Now().Add(-5 * time.Minute)
255+ createdFuture := time.Now().Add(5 * time.Minute)
256+
257+ DescribeTable("with a session",
258+ func(in refreshSessionIfNeededTableInput) {
259+ refreshed := false
260+ validated := false
261+
262+ s := &storedSessionLoader{
263+ refreshPeriod: in.refreshPeriod,
264+ store: &fakeSessionStore{},
265+ refreshSessionWithProviderIfNeeded: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
266+ refreshed = true
267+ switch ss.RefreshToken {
268+ case refresh:
269+ return true, nil
270+ case noRefresh:
271+ return false, nil
272+ default:
273+ return false, errors.New("error refreshing session")
274+ }
275+ },
276+ validateSessionState: func(_ context.Context, ss *sessionsapi.SessionState) bool {
277+ validated = true
278+ return ss.AccessToken != "Invalid"
279+ },
280+ }
281+
282+ req := httptest.NewRequest("", "/", nil)
283+ err := s.refreshSessionIfNeeded(nil, req, in.session)
284+ if in.expectedErr != nil {
285+ Expect(err).To(MatchError(in.expectedErr))
286+ } else {
287+ Expect(err).ToNot(HaveOccurred())
288+ }
289+ Expect(refreshed).To(Equal(in.expectRefreshed))
290+ Expect(validated).To(Equal(in.expectValidated))
291+ },
292+ Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
293+ refreshPeriod: time.Duration(0),
294+ session: &sessionsapi.SessionState{
295+ RefreshToken: refresh,
296+ CreatedAt: &createdFuture,
297+ },
298+ expectedErr: nil,
299+ expectRefreshed: false,
300+ expectValidated: false,
301+ }),
302+ Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
303+ refreshPeriod: time.Duration(0),
304+ session: &sessionsapi.SessionState{
305+ RefreshToken: refresh,
306+ CreatedAt: &createdPast,
307+ },
308+ expectedErr: nil,
309+ expectRefreshed: false,
310+ expectValidated: false,
311+ }),
312+ Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
313+ refreshPeriod: 1 * time.Minute,
314+ session: &sessionsapi.SessionState{
315+ RefreshToken: refresh,
316+ CreatedAt: &createdFuture,
317+ },
318+ expectedErr: nil,
319+ expectRefreshed: false,
320+ expectValidated: false,
321+ }),
322+ Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
323+ refreshPeriod: 1 * time.Minute,
324+ session: &sessionsapi.SessionState{
325+ RefreshToken: refresh,
326+ CreatedAt: &createdPast,
327+ },
328+ expectedErr: nil,
329+ expectRefreshed: true,
330+ expectValidated: false,
331+ }),
332+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
333+ refreshPeriod: 1 * time.Minute,
334+ session: &sessionsapi.SessionState{
335+ RefreshToken: noRefresh,
336+ CreatedAt: &createdPast,
337+ ExpiresOn: &createdFuture,
338+ },
339+ expectedErr: nil,
340+ expectRefreshed: true,
341+ expectValidated: true,
342+ }),
343+ Entry("when the provider refresh fails", refreshSessionIfNeededTableInput{
344+ refreshPeriod: 1 * time.Minute,
345+ session: &sessionsapi.SessionState{
346+ RefreshToken: "RefreshError",
347+ CreatedAt: &createdPast,
348+ },
349+ expectedErr: errors.New("error refreshing access token: error refreshing session"),
350+ expectRefreshed: true,
351+ expectValidated: false,
352+ }),
353+ Entry("when the session is not refreshed by the provider and validation fails", refreshSessionIfNeededTableInput{
354+ refreshPeriod: 1 * time.Minute,
355+ session: &sessionsapi.SessionState{
356+ AccessToken: "Invalid",
357+ RefreshToken: noRefresh,
358+ CreatedAt: &createdPast,
359+ ExpiresOn: &createdFuture,
360+ },
361+ expectedErr: errors.New("session is invalid"),
362+ expectRefreshed: true,
363+ expectValidated: true,
364+ }),
365+ )
366+ })
367+
368+ Context("refreshSessionWithProvider", func() {
369+ type refreshSessionWithProviderTableInput struct {
370+ session *sessionsapi.SessionState
371+ expectedErr error
372+ expectRefreshed bool
373+ expectSaved bool
374+ }
375+
376+ now := time.Now()
377+
378+ DescribeTable("when refreshing with the provider",
379+ func(in refreshSessionWithProviderTableInput) {
380+ saved := false
381+
382+ s := &storedSessionLoader{
383+ store: &fakeSessionStore{
384+ SaveFunc: func(_ http.ResponseWriter, _ *http.Request, ss *sessionsapi.SessionState) error {
385+ saved = true
386+ if ss.AccessToken == "NoSave" {
387+ return errors.New("unable to save session")
388+ }
389+ return nil
390+ },
391+ },
392+ refreshSessionWithProviderIfNeeded: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
393+ switch ss.RefreshToken {
394+ case refresh:
395+ return true, nil
396+ case noRefresh:
397+ return false, nil
398+ default:
399+ return false, errors.New("error refreshing session")
400+ }
401+ },
402+ }
403+
404+ req := httptest.NewRequest("", "/", nil)
405+ refreshed, err := s.refreshSessionWithProvider(nil, req, in.session)
406+ if in.expectedErr != nil {
407+ Expect(err).To(MatchError(in.expectedErr))
408+ } else {
409+ Expect(err).ToNot(HaveOccurred())
410+ }
411+ Expect(refreshed).To(Equal(in.expectRefreshed))
412+ Expect(saved).To(Equal(in.expectSaved))
413+ },
414+ Entry("when the provider does not refresh the session", refreshSessionWithProviderTableInput{
415+ session: &sessionsapi.SessionState{
416+ RefreshToken: noRefresh,
417+ },
418+ expectedErr: nil,
419+ expectRefreshed: false,
420+ expectSaved: false,
421+ }),
422+ Entry("when the provider refreshes the session", refreshSessionWithProviderTableInput{
423+ session: &sessionsapi.SessionState{
424+ RefreshToken: refresh,
425+ },
426+ expectedErr: nil,
427+ expectRefreshed: true,
428+ expectSaved: true,
429+ }),
430+ Entry("when the provider returns an error", refreshSessionWithProviderTableInput{
431+ session: &sessionsapi.SessionState{
432+ RefreshToken: "RefreshError",
433+ CreatedAt: &now,
434+ ExpiresOn: &now,
435+ },
436+ expectedErr: errors.New("error refreshing access token: error refreshing session"),
437+ expectRefreshed: false,
438+ expectSaved: false,
439+ }),
440+ Entry("when the saving the session returns an error", refreshSessionWithProviderTableInput{
441+ session: &sessionsapi.SessionState{
442+ RefreshToken: refresh,
443+ AccessToken: "NoSave",
444+ },
445+ expectedErr: errors.New("error saving session: unable to save session"),
446+ expectRefreshed: false,
447+ expectSaved: true,
448+ }),
449+ )
450+ })
451+
452+ Context("validateSession", func() {
453+ var s *storedSessionLoader
454+
455+ BeforeEach(func() {
456+ s = &storedSessionLoader{
457+ validateSessionState: func(_ context.Context, ss *sessionsapi.SessionState) bool {
458+ return ss.AccessToken == "Valid"
459+ },
460+ }
461+ })
462+
463+ Context("with a valid session", func() {
464+ It("does not return an error", func() {
465+ expires := time.Now().Add(1 * time.Minute)
466+ session := &sessionsapi.SessionState{
467+ AccessToken: "Valid",
468+ ExpiresOn: &expires,
469+ }
470+ Expect(s.validateSession(ctx, session)).To(Succeed())
471+ })
472+ })
473+
474+ Context("with an expired session", func() {
475+ It("returns an error", func() {
476+ created := time.Now().Add(-5 * time.Minute)
477+ expires := time.Now().Add(-1 * time.Minute)
478+ session := &sessionsapi.SessionState{
479+ AccessToken: "Valid",
480+ CreatedAt: &created,
481+ ExpiresOn: &expires,
482+ }
483+ Expect(s.validateSession(ctx, session)).To(MatchError("session is expired"))
484+ })
485+ })
486+
487+ Context("with an invalid session", func() {
488+ It("returns an error", func() {
489+ expires := time.Now().Add(1 * time.Minute)
490+ session := &sessionsapi.SessionState{
491+ AccessToken: "Invalid",
492+ ExpiresOn: &expires,
493+ }
494+ Expect(s.validateSession(ctx, session)).To(MatchError("session is invalid"))
495+ })
496+ })
497+ })
498+})
499+
500+type fakeSessionStore struct {
501+ SaveFunc func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error
502+ LoadFunc func(req *http.Request) (*sessionsapi.SessionState, error)
503+ ClearFunc func(rw http.ResponseWriter, req *http.Request) error
504+}
505+
506+func (f *fakeSessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
507+ if f.SaveFunc != nil {
508+ return f.SaveFunc(rw, req, s)
509+ }
510+ return nil
511+}
512+func (f *fakeSessionStore) Load(req *http.Request) (*sessionsapi.SessionState, error) {
513+ if f.LoadFunc != nil {
514+ return f.LoadFunc(req)
515+ }
516+ return nil, nil
517+}
518+
519+func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
520+ if f.ClearFunc != nil {
521+ return f.ClearFunc(rw, req)
522+ }
523+ return nil
524+}
providers/provider_default.go+3−30
@@ -126,35 +126,8 @@ func (p *ProviderData) RefreshSessionIfNeeded(ctx context.Context, s *sessions.S
126126 return false, nil
127127}
128128
129+// CreateSessionStateFromBearerToken should be implemented to allow providers
130+// to convert ID tokens into sessions
129131func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) {
130− var claims struct {
131− Subject string `json:"sub"`
132− Email string `json:"email"`
133− Verified *bool `json:"email_verified"`
134− PreferredUsername string `json:"preferred_username"`
135− }
136−
137− if err := idToken.Claims(&claims); err != nil {
138− return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
139− }
140−
141− if claims.Email == "" {
142− claims.Email = claims.Subject
143− }
144−
145− if claims.Verified != nil && !*claims.Verified {
146− return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
147− }
148−
149− newSession := &sessions.SessionState{
150− Email: claims.Email,
151− User: claims.Subject,
152− PreferredUsername: claims.PreferredUsername,
153− AccessToken: rawIDToken,
154− IDToken: rawIDToken,
155− RefreshToken: "",
156− ExpiresOn: &idToken.Expiry,
157− }
158−
159− return newSession, nil
132+ return nil, errors.New("not implemented")
160133}
providers/provider_default_test.go−41
@@ -2,15 +2,10 @@ package providers
22
33import (
44 "context"
5− "crypto/rand"
6− "crypto/rsa"
75 "net/url"
86 "testing"
97 "time"
108
11− "github.com/coreos/go-oidc"
12− "github.com/dgrijalva/jwt-go"
13−
149 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1510 "github.com/stretchr/testify/assert"
1611)
@@ -52,39 +47,3 @@ func TestAcrValuesConfigured(t *testing.T) {
5247 result := p.GetLoginURL("https://my.test.app/oauth", "")
5348 assert.Contains(t, result, "acr_values=testValue")
5449}
55−
56−func TestCreateSessionStateFromBearerToken(t *testing.T) {
57− minimalIDToken := jwt.StandardClaims{
58− Audience: "asdf1234",
59− ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
60− Id: "id-some-id",
61− IssuedAt: time.Now().Unix(),
62− Issuer: "https://issuer.example.com",
63− NotBefore: 0,
64− Subject: "123456789",
65− }
66− // From oidc_test.go
67− verifier := oidc.NewVerifier(
68− "https://issuer.example.com",
69− fakeKeySetStub{},
70− &oidc.Config{ClientID: "asdf1234"},
71− )
72−
73− key, err := rsa.GenerateKey(rand.Reader, 2048)
74− assert.NoError(t, err)
75− rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key)
76− assert.NoError(t, err)
77− // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
78− idToken, err := verifier.Verify(context.Background(), rawIDToken)
79− assert.NoError(t, err)
80−
81− session, err := (*ProviderData)(nil).CreateSessionStateFromBearerToken(context.Background(), rawIDToken, idToken)
82− assert.NoError(t, err)
83−
84− assert.Equal(t, rawIDToken, session.AccessToken)
85− assert.Equal(t, rawIDToken, session.IDToken)
86− assert.Equal(t, "123456789", session.Email)
87− assert.Equal(t, "123456789", session.User)
88− assert.Empty(t, session.RefreshToken)
89− assert.Empty(t, session.PreferredUsername)
90−}