git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #14 from pusher/oidc

OIDC ID Token, Authorization Headers, Refreshing and Verification

commit440d2f32bff2b023f34cc4aa78752e99a956789c authorJoel Speed <Joel.speed@hotmail.co.uk> dateTue 22 Jan 2019, 15:56 (8 years ago) parentc8ca0c80925b88
10 files changed+227−40
CHANGELOG.md+5 README.md+2 main.go+2 oauthproxy.go+115−12 oauthproxy_test.go+7−4 options.go+4 providers/google.go+1 providers/oidc.go+64−16 providers/session_state.go+22−6 providers/session_state_test.go+5−2
CHANGELOG.md+5
@@ -2,6 +2,11 @@
22
33## Changes since v3.0.0
44
5+- [#14](https://github.com/pusher/oauth2_proxy/pull/14) OIDC ID Token, Authorization Headers, Refreshing and Verification (@joelspeed)
6+ - Implement `pass-authorization-header` and `set-authorization-header` flags
7+ - Implement token refreshing in OIDC provider
8+ - Split cookies larger than 4k limit into multiple cookies
9+ - Implement token validation in OIDC provider
510- [#21](https://github.com/pusher/oauth2_proxy/pull/21) Docker Improvement (@yaegashi)
611 - Move Docker base image from debian to alpine
712 - Install ca-certificates in docker image
README.md+2
@@ -212,6 +212,7 @@ Usage of oauth2_proxy:
212212 -https-address string: <addr>:<port> to listen on for HTTPS clients (default ":443")
213213 -login-url string: Authentication endpoint
214214 -pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
215+ -pass-authorization-header: pass OIDC IDToken to upstream via Authorization Bearer header
215216 -pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
216217 -pass-host-header: pass the request Host Header to upstream (default true)
217218 -pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true)
@@ -225,6 +226,7 @@ Usage of oauth2_proxy:
225226 -resource string: The resource that is protected (Azure AD only)
226227 -scope string: OAuth scope specification
227228 -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
229+ -set-authorization-header: set Authorization Bearer response header (useful in Nginx auth_request mode)
228230 -signature-key string: GAP-Signature request signature key (algorithm:secretkey)
229231 -skip-auth-preflight: will skip authentication for OPTIONS requests
230232 -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
main.go+2
@@ -37,6 +37,8 @@ func main() {
3737 flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
3838 flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
3939 flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream")
40+ flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream")
41+ flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)")
4042 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
4143 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
4244 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
oauthproxy.go+115−12
@@ -26,6 +26,10 @@ const (
2626
2727 httpScheme = "http"
2828 httpsScheme = "https"
29+
30+ // Cookies are limited to 4kb including the length of the cookie name,
31+ // the cookie name can be up to 256 bytes
32+ maxCookieLength = 3840
2933)
3034
3135// SignatureHeaders contains the headers to be signed by the hmac algorithm
@@ -76,6 +80,8 @@ type OAuthProxy struct {
7680 PassUserHeaders bool
7781 BasicAuthPassword string
7882 PassAccessToken bool
83+ SetAuthorization bool
84+ PassAuthorization bool
7985 CookieCipher *cookie.Cipher
8086 skipAuthRegex []string
8187 skipAuthPreflight bool
@@ -183,7 +189,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
183189 log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, refresh)
184190
185191 var cipher *cookie.Cipher
186− if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
192+ if opts.PassAccessToken || opts.SetAuthorization || opts.PassAuthorization || (opts.CookieRefresh != time.Duration(0)) {
187193 var err error
188194 cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
189195 if err != nil {
@@ -222,6 +228,8 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
222228 PassUserHeaders: opts.PassUserHeaders,
223229 BasicAuthPassword: opts.BasicAuthPassword,
224230 PassAccessToken: opts.PassAccessToken,
231+ SetAuthorization: opts.SetAuthorization,
232+ PassAuthorization: opts.PassAuthorization,
225233 SkipProviderButton: opts.SkipProviderButton,
226234 CookieCipher: cipher,
227235 templates: loadTemplates(opts.CustomTemplatesDir),
@@ -278,15 +286,100 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e
278286
279287// MakeSessionCookie creates an http.Cookie containing the authenticated user's
280288// authentication details
281−func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
289+func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
282290 if value != "" {
283291 value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
284− if len(value) > 4096 {
285− // Cookies cannot be larger than 4kb
286− log.Printf("WARNING - Cookie Size: %d bytes", len(value))
292+ }
293+ c := p.makeCookie(req, p.CookieName, value, expiration, now)
294+ if len(c.Value) > 4096-len(p.CookieName) {
295+ return splitCookie(c)
296+ }
297+ return []*http.Cookie{c}
298+}
299+
300+func copyCookie(c *http.Cookie) *http.Cookie {
301+ return &http.Cookie{
302+ Name: c.Name,
303+ Value: c.Value,
304+ Path: c.Path,
305+ Domain: c.Domain,
306+ Expires: c.Expires,
307+ RawExpires: c.RawExpires,
308+ MaxAge: c.MaxAge,
309+ Secure: c.Secure,
310+ HttpOnly: c.HttpOnly,
311+ Raw: c.Raw,
312+ Unparsed: c.Unparsed,
313+ }
314+}
315+
316+// splitCookie reads the full cookie generated to store the session and splits
317+// it into a slice of cookies which fit within the 4kb cookie limit indexing
318+// the cookies from 0
319+func splitCookie(c *http.Cookie) []*http.Cookie {
320+ if len(c.Value) < maxCookieLength {
321+ return []*http.Cookie{c}
322+ }
323+ cookies := []*http.Cookie{}
324+ valueBytes := []byte(c.Value)
325+ count := 0
326+ for len(valueBytes) > 0 {
327+ new := copyCookie(c)
328+ new.Name = fmt.Sprintf("%s-%d", c.Name, count)
329+ count++
330+ if len(valueBytes) < maxCookieLength {
331+ new.Value = string(valueBytes)
332+ valueBytes = []byte{}
333+ } else {
334+ newValue := valueBytes[:maxCookieLength]
335+ valueBytes = valueBytes[maxCookieLength:]
336+ new.Value = string(newValue)
337+ }
338+ cookies = append(cookies, new)
339+ }
340+ return cookies
341+}
342+
343+// joinCookies takes a slice of cookies from the request and reconstructs the
344+// full session cookie
345+func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
346+ if len(cookies) == 0 {
347+ return nil, fmt.Errorf("list of cookies must be > 0")
348+ }
349+ if len(cookies) == 1 {
350+ return cookies[0], nil
351+ }
352+ c := copyCookie(cookies[0])
353+ for i := 1; i < len(cookies); i++ {
354+ c.Value += cookies[i].Value
355+ }
356+ c.Name = strings.TrimRight(c.Name, "-0")
357+ return c, nil
358+}
359+
360+// loadCookie retreieves the sessions state cookie from the http request.
361+// If a single cookie is present this will be returned, otherwise it attempts
362+// to reconstruct a cookie split up by splitCookie
363+func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
364+ c, err := req.Cookie(cookieName)
365+ if err == nil {
366+ return c, nil
367+ }
368+ cookies := []*http.Cookie{}
369+ err = nil
370+ count := 0
371+ for err == nil {
372+ var c *http.Cookie
373+ c, err = req.Cookie(fmt.Sprintf("%s-%d", cookieName, count))
374+ if err == nil {
375+ cookies = append(cookies, c)
376+ count++
287377 }
288378 }
289− return p.makeCookie(req, p.CookieName, value, expiration, now)
379+ if len(cookies) == 0 {
380+ return nil, fmt.Errorf("Could not find cookie %s", cookieName)
381+ }
382+ return joinCookies(cookies)
290383}
291384
292385// MakeCSRFCookie creates a cookie for CSRF
@@ -330,12 +423,14 @@ func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, va
330423// ClearSessionCookie creates a cookie to unset the user's authentication cookie
331424// stored in the user's session
332425func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {
333− clr := p.MakeSessionCookie(req, "", time.Hour*-1, time.Now())
334− http.SetCookie(rw, clr)
426+ cookies := p.MakeSessionCookie(req, "", time.Hour*-1, time.Now())
427+ for _, clr := range cookies {
428+ http.SetCookie(rw, clr)
429+ }
335430
336431 // ugly hack because default domain changed
337− if p.CookieDomain == "" {
338− clr2 := *clr
432+ if p.CookieDomain == "" && len(cookies) > 0 {
433+ clr2 := *cookies[0]
339434 clr2.Domain = req.Host
340435 http.SetCookie(rw, &clr2)
341436 }
@@ -343,13 +438,15 @@ func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Reques
343438
344439// SetSessionCookie adds the user's session cookie to the response
345440func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
346− http.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()))
441+ for _, c := range p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()) {
442+ http.SetCookie(rw, c)
443+ }
347444}
348445
349446// LoadCookiedSession reads the user's authentication details from the request
350447func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
351448 var age time.Duration
352− c, err := req.Cookie(p.CookieName)
449+ c, err := loadCookie(req, p.CookieName)
353450 if err != nil {
354451 // always http.ErrNoCookie
355452 return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
@@ -750,6 +847,12 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
750847 if p.PassAccessToken && session.AccessToken != "" {
751848 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
752849 }
850+ if p.PassAuthorization && session.IDToken != "" {
851+ req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
852+ }
853+ if p.SetAuthorization && session.IDToken != "" {
854+ rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
855+ }
753856 if session.Email == "" {
754857 rw.Header().Set("GAP-Auth", session.User)
755858 } else {
oauthproxy_test.go+7−4
@@ -502,7 +502,7 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
502502 })
503503}
504504
505−func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) *http.Cookie {
505+func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cookie {
506506 return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref)
507507}
508508
@@ -511,7 +511,9 @@ func (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time
511511 if err != nil {
512512 return err
513513 }
514− p.req.AddCookie(p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref))
514+ for _, c := range p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref) {
515+ p.req.AddCookie(c)
516+ }
515517 return nil
516518}
517519
@@ -800,8 +802,9 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
800802 if err != nil {
801803 panic(err)
802804 }
803− cookie := proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now())
804− req.AddCookie(cookie)
805+ for _, c := range proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) {
806+ req.AddCookie(c)
807+ }
805808 // This is used by the upstream to validate the signature.
806809 st.authenticator.auth = hmacauth.NewHmacAuth(
807810 crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)
options.go+4
@@ -61,6 +61,8 @@ type Options struct {
6161 PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"`
6262 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
6363 SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
64+ SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header"`
65+ PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header"`
6466 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
6567
6668 // These options allow for other providers besides Google, with
@@ -113,6 +115,8 @@ func NewOptions() *Options {
113115 PassUserHeaders: true,
114116 PassAccessToken: false,
115117 PassHostHeader: true,
118+ SetAuthorization: false,
119+ PassAuthorization: false,
116120 ApprovalPrompt: "force",
117121 RequestLogging: true,
118122 RequestLoggingFormat: defaultRequestLoggingFormat,
providers/google.go+1
@@ -145,6 +145,7 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err
145145 }
146146 s = &SessionState{
147147 AccessToken: jsonResponse.AccessToken,
148+ IDToken: jsonResponse.IDToken,
148149 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
149150 RefreshToken: jsonResponse.RefreshToken,
150151 Email: email,
providers/oidc.go+64−16
@@ -38,7 +38,61 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err er
3838 if err != nil {
3939 return nil, fmt.Errorf("token exchange: %v", err)
4040 }
41+ s, err = p.createSessionState(ctx, token)
42+ if err != nil {
43+ return nil, fmt.Errorf("unable to update session: %v", err)
44+ }
45+ return
46+}
47+
48+// RefreshSessionIfNeeded checks if the session has expired and uses the
49+// RefreshToken to fetch a new ID token if required
50+func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
51+ if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
52+ return false, nil
53+ }
4154
55+ origExpiration := s.ExpiresOn
56+
57+ err := p.redeemRefreshToken(s)
58+ if err != nil {
59+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
60+ }
61+
62+ fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
63+ return true, nil
64+}
65+
66+func (p *OIDCProvider) redeemRefreshToken(s *SessionState) (err error) {
67+ c := oauth2.Config{
68+ ClientID: p.ClientID,
69+ ClientSecret: p.ClientSecret,
70+ Endpoint: oauth2.Endpoint{
71+ TokenURL: p.RedeemURL.String(),
72+ },
73+ }
74+ ctx := context.Background()
75+ t := &oauth2.Token{
76+ RefreshToken: s.RefreshToken,
77+ Expiry: time.Now().Add(-time.Hour),
78+ }
79+ token, err := c.TokenSource(ctx, t).Token()
80+ if err != nil {
81+ return fmt.Errorf("failed to get token: %v", err)
82+ }
83+ newSession, err := p.createSessionState(ctx, token)
84+ if err != nil {
85+ return fmt.Errorf("unable to update session: %v", err)
86+ }
87+ s.AccessToken = newSession.AccessToken
88+ s.IDToken = newSession.IDToken
89+ s.RefreshToken = newSession.RefreshToken
90+ s.ExpiresOn = newSession.ExpiresOn
91+ s.Email = newSession.Email
92+ return
93+}
94+
95+func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*SessionState, error) {
4296 rawIDToken, ok := token.Extra("id_token").(string)
4397 if !ok {
4498 return nil, fmt.Errorf("token response did not contain an id_token")
@@ -66,28 +120,22 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err er
66120 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
67121 }
68122
69− s = &SessionState{
123+ return &SessionState{
70124 AccessToken: token.AccessToken,
125+ IDToken: rawIDToken,
71126 RefreshToken: token.RefreshToken,
72127 ExpiresOn: token.Expiry,
73128 Email: claims.Email,
74− }
75−
76− return
129+ }, nil
77130}
78131
79−// RefreshSessionIfNeeded checks if the session has expired and uses the
80−// RefreshToken to fetch a new ID token if required
81−//
82−// WARNGING: This implementation is broken and does not check with the upstream
83−// OIDC provider before refreshing the session
84−func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
85− if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
86− return false, nil
132+// ValidateSessionState checks that the session's IDToken is still valid
133+func (p *OIDCProvider) ValidateSessionState(s *SessionState) bool {
134+ ctx := context.Background()
135+ _, err := p.Verifier.Verify(ctx, s.IDToken)
136+ if err != nil {
137+ return false
87138 }
88139
89− origExpiration := s.ExpiresOn
90− s.ExpiresOn = time.Now().Add(time.Second).Truncate(time.Second)
91− fmt.Printf("refreshed access token %s (expired on %s)\n", s, origExpiration)
92− return false, nil
140+ return true
93141}
providers/session_state.go+22−6
@@ -12,6 +12,7 @@ import (
1212// SessionState is used to store information about the currently authenticated user session
1313type SessionState struct {
1414 AccessToken string
15+ IDToken string
1516 ExpiresOn time.Time
1617 RefreshToken string
1718 Email string
@@ -32,6 +33,9 @@ func (s *SessionState) String() string {
3233 if s.AccessToken != "" {
3334 o += " token:true"
3435 }
36+ if s.IDToken != "" {
37+ o += " id_token:true"
38+ }
3539 if !s.ExpiresOn.IsZero() {
3640 o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
3741 }
@@ -65,13 +69,19 @@ func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
6569 return "", err
6670 }
6771 }
72+ i := s.IDToken
73+ if i != "" {
74+ if i, err = c.Encrypt(i); err != nil {
75+ return "", err
76+ }
77+ }
6878 r := s.RefreshToken
6979 if r != "" {
7080 if r, err = c.Encrypt(r); err != nil {
7181 return "", err
7282 }
7383 }
74− return fmt.Sprintf("%s|%s|%d|%s", s.accountInfo(), a, s.ExpiresOn.Unix(), r), nil
84+ return fmt.Sprintf("%s|%s|%s|%d|%s", s.accountInfo(), a, i, s.ExpiresOn.Unix(), r), nil
7585}
7686
7787func decodeSessionStatePlain(v string) (s *SessionState, err error) {
@@ -96,8 +106,8 @@ func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error)
96106 }
97107
98108 chunks := strings.Split(v, "|")
99− if len(chunks) != 4 {
100− err = fmt.Errorf("invalid number of fields (got %d expected 4)", len(chunks))
109+ if len(chunks) != 5 {
110+ err = fmt.Errorf("invalid number of fields (got %d expected 5)", len(chunks))
101111 return
102112 }
103113
@@ -112,11 +122,17 @@ func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error)
112122 }
113123 }
114124
115− ts, _ := strconv.Atoi(chunks[2])
125+ if chunks[2] != "" {
126+ if sessionState.IDToken, err = c.Decrypt(chunks[2]); err != nil {
127+ return nil, err
128+ }
129+ }
130+
131+ ts, _ := strconv.Atoi(chunks[3])
116132 sessionState.ExpiresOn = time.Unix(int64(ts), 0)
117133
118− if chunks[3] != "" {
119− if sessionState.RefreshToken, err = c.Decrypt(chunks[3]); err != nil {
134+ if chunks[4] != "" {
135+ if sessionState.RefreshToken, err = c.Decrypt(chunks[4]); err != nil {
120136 return nil, err
121137 }
122138 }
providers/session_state_test.go+5−2
@@ -21,12 +21,13 @@ func TestSessionStateSerialization(t *testing.T) {
2121 s := &SessionState{
2222 Email: "user@domain.com",
2323 AccessToken: "token1234",
24+ IDToken: "rawtoken1234",
2425 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
2526 RefreshToken: "refresh4321",
2627 }
2728 encoded, err := s.EncodeSessionState(c)
2829 assert.Equal(t, nil, err)
29− assert.Equal(t, 3, strings.Count(encoded, "|"))
30+ assert.Equal(t, 4, strings.Count(encoded, "|"))
3031
3132 ss, err := DecodeSessionState(encoded, c)
3233 t.Logf("%#v", ss)
@@ -34,6 +35,7 @@ func TestSessionStateSerialization(t *testing.T) {
3435 assert.Equal(t, "user", ss.User)
3536 assert.Equal(t, s.Email, ss.Email)
3637 assert.Equal(t, s.AccessToken, ss.AccessToken)
38+ assert.Equal(t, s.IDToken, ss.IDToken)
3739 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
3840 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
3941
@@ -45,6 +47,7 @@ func TestSessionStateSerialization(t *testing.T) {
4547 assert.Equal(t, s.Email, ss.Email)
4648 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
4749 assert.NotEqual(t, s.AccessToken, ss.AccessToken)
50+ assert.NotEqual(t, s.IDToken, ss.IDToken)
4851 assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
4952}
5053
@@ -62,7 +65,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
6265 }
6366 encoded, err := s.EncodeSessionState(c)
6467 assert.Equal(t, nil, err)
65− assert.Equal(t, 3, strings.Count(encoded, "|"))
68+ assert.Equal(t, 4, strings.Count(encoded, "|"))
6669
6770 ss, err := DecodeSessionState(encoded, c)
6871 t.Logf("%#v", ss)