Skip to content

Commit

Permalink
filters/auth: add grant cookie encoder
Browse files Browse the repository at this point in the history
Add CookerEncoder interface to allow custom implementation of grant
cookie encoding.

For example custom implementation may store token value in some
permanent key-value storage and encode key into the cookie.

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
  • Loading branch information
AlexanderYastrebov committed Mar 18, 2024
1 parent e56979a commit e42a740
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 43 deletions.
35 changes: 13 additions & 22 deletions filters/auth/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,10 @@ func loginRedirectWithOverride(ctx filters.FilterContext, config *OAuthConfig, o
})
}

func (f *grantFilter) refreshToken(c *cookie, req *http.Request) (*oauth2.Token, error) {
func (f *grantFilter) refreshToken(token *oauth2.Token, req *http.Request) (*oauth2.Token, error) {
// Set the expiry of the token to the past to trigger oauth2.TokenSource
// to refresh the access token.
token := &oauth2.Token{
AccessToken: c.AccessToken,
RefreshToken: c.RefreshToken,
Expiry: time.Now().Add(-time.Minute),
}
token.Expiry = time.Now().Add(-time.Minute)

ctx := providerContext(f.config)

Expand All @@ -114,12 +110,12 @@ func (f *grantFilter) refreshToken(c *cookie, req *http.Request) (*oauth2.Token,
return tokenSource.Token()
}

func (f *grantFilter) refreshTokenIfRequired(c *cookie, ctx filters.FilterContext) (*oauth2.Token, error) {
canRefresh := c.RefreshToken != ""
func (f *grantFilter) refreshTokenIfRequired(t *oauth2.Token, ctx filters.FilterContext) (*oauth2.Token, error) {
canRefresh := t.RefreshToken != ""

if c.isAccessTokenExpired() {
if time.Now().After(t.Expiry) {
if canRefresh {
token, err := f.refreshToken(c, ctx.Request())
token, err := f.refreshToken(t, ctx.Request())
if err == nil {
// Remember that this token was just successfully refreshed
// so that we can send an updated cookie in the response.
Expand All @@ -130,12 +126,7 @@ func (f *grantFilter) refreshTokenIfRequired(c *cookie, ctx filters.FilterContex
return nil, errExpiredToken
}
} else {
return &oauth2.Token{
AccessToken: c.AccessToken,
TokenType: "Bearer",
RefreshToken: c.RefreshToken,
Expiry: c.Expiry,
}, nil
return t, nil
}
}

Expand Down Expand Up @@ -179,15 +170,13 @@ func (f *grantFilter) setupToken(token *oauth2.Token, tokeninfo map[string]inter
}

func (f *grantFilter) Request(ctx filters.FilterContext) {
req := ctx.Request()

c, err := extractCookie(req, f.config)
token, err := f.config.GrantCookieEncoder.Read(f.config, ctx.Request())
if err == http.ErrNoCookie {
loginRedirect(ctx, f.config)
return
}

token, err := f.refreshTokenIfRequired(c, ctx)
token, err = f.refreshTokenIfRequired(token, ctx)
if err != nil {
// Refresh failed and we no longer have a valid access token.
loginRedirect(ctx, f.config)
Expand Down Expand Up @@ -221,11 +210,13 @@ func (f *grantFilter) Response(ctx filters.FilterContext) {
return
}

c, err := createCookie(f.config, ctx.Request().Host, token)
cookies, err := f.config.GrantCookieEncoder.Update(f.config, ctx.Request(), token)
if err != nil {
ctx.Logger().Errorf("Failed to generate cookie: %v.", err)
return
}

ctx.Response().Header.Add("Set-Cookie", c.String())
for _, c := range cookies {
ctx.Response().Header.Add("Set-Cookie", c.String())
}
}
13 changes: 8 additions & 5 deletions filters/auth/grantcallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,23 @@ func (f *grantCallbackFilter) Request(ctx filters.FilterContext) {
return
}

c, err := createCookie(f.config, req.Host, token)
cookies, err := f.config.GrantCookieEncoder.Update(f.config, req, token)
if err != nil {
ctx.Logger().Errorf("Failed to create OAuth grant cookie: %v.", err)
serverError(ctx)
return
}

ctx.Serve(&http.Response{
resp := &http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{
"Location": []string{state.RequestURL},
"Set-Cookie": []string{c.String()},
"Location": []string{state.RequestURL},
},
})
}
for _, c := range cookies {
resp.Header.Add("Set-Cookie", c.String())
}
ctx.Serve(resp)
}

func (f *grantCallbackFilter) Response(ctx filters.FilterContext) {}
7 changes: 7 additions & 0 deletions filters/auth/grantconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ type OAuthConfig struct {
// GrantTokeninfoKeys, optional. When not empty, keys not in this list are removed from the tokeninfo map.
GrantTokeninfoKeys []string

// GrantCookieEncoder, optional. Cookie encoder stores and extracts OAuth token from cookies.
GrantCookieEncoder CookieEncoder

// TokeninfoSubjectKey, optional. When set, it is used to look up the subject
// ID in the tokeninfo map received from a tokeninfo endpoint request.
TokeninfoSubjectKey string
Expand Down Expand Up @@ -257,6 +260,10 @@ func (c *OAuthConfig) Init() error {
}
}

if c.GrantCookieEncoder == nil {
c.GrantCookieEncoder = encryptedCookieEncoder{}
}

c.initialized = true
return nil
}
Expand Down
45 changes: 40 additions & 5 deletions filters/auth/grantcookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ import (
"golang.org/x/oauth2"
)

type CookieEncoder interface {
// Update creates a set of cookies that encodes the token and deletes previously existing cookies if necessary.
// When token is nil it only returns cookies to delete.
Update(config *OAuthConfig, request *http.Request, token *oauth2.Token) ([]*http.Cookie, error)

// Read extracts the token from the request cookies.
Read(config *OAuthConfig, request *http.Request) (*oauth2.Token, error)
}

type encryptedCookieEncoder struct{}

var _ CookieEncoder = encryptedCookieEncoder{}

func (encryptedCookieEncoder) Update(config *OAuthConfig, request *http.Request, token *oauth2.Token) ([]*http.Cookie, error) {
if token != nil {
c, err := createCookie(config, request.Host, token)
if err != nil {
return nil, err
}
return []*http.Cookie{c}, nil
} else {
c := createDeleteCookie(config, request.Host)
return []*http.Cookie{c}, nil
}
}

func (encryptedCookieEncoder) Read(config *OAuthConfig, request *http.Request) (*oauth2.Token, error) {
c, err := extractCookie(request, config)
if err != nil {
return nil, err
}

return &oauth2.Token{
AccessToken: c.AccessToken,
TokenType: "Bearer",
RefreshToken: c.RefreshToken,
Expiry: c.Expiry,
}, nil
}

type cookie struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Expand Down Expand Up @@ -39,11 +79,6 @@ func decodeCookie(cookieHeader string, config *OAuthConfig) (c *cookie, err erro
return
}

func (c *cookie) isAccessTokenExpired() bool {
now := time.Now()
return now.After(c.Expiry)
}

// allowedForHost checks if provided host matches cookie domain
// according to https://www.rfc-editor.org/rfc/rfc6265#section-5.1.3
func (c *cookie) allowedForHost(host string) bool {
Expand Down
5 changes: 2 additions & 3 deletions filters/auth/grantcookie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ const (
)

func newGrantCookies(t *testing.T, config *OAuthConfig, host string, token oauth2.Token) []*http.Cookie {
cookie, err := createCookie(config, host, &token)
cookies, err := config.GrantCookieEncoder.Update(config, &http.Request{Host: host}, &token)
require.NoError(t, err)

return []*http.Cookie{cookie}
return cookies
}

func NewGrantCookies(t *testing.T, config *OAuthConfig) []*http.Cookie {
Expand Down
22 changes: 14 additions & 8 deletions filters/auth/grantlogout.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (f *grantLogoutFilter) Request(ctx filters.FilterContext) {

req := ctx.Request()

c, err := extractCookie(req, f.config)
token, err := f.config.GrantCookieEncoder.Read(f.config, req)
if err != nil {
unauthorized(
ctx,
Expand All @@ -136,7 +136,7 @@ func (f *grantLogoutFilter) Request(ctx filters.FilterContext) {
return
}

if c.AccessToken == "" && c.RefreshToken == "" {
if token.AccessToken == "" && token.RefreshToken == "" {
unauthorized(
ctx,
"",
Expand All @@ -153,15 +153,15 @@ func (f *grantLogoutFilter) Request(ctx filters.FilterContext) {
}

var accessTokenRevokeError, refreshTokenRevokeError error
if c.AccessToken != "" {
accessTokenRevokeError = f.revokeTokenType(authConfig, accessTokenType, c.AccessToken)
if token.AccessToken != "" {
accessTokenRevokeError = f.revokeTokenType(authConfig, accessTokenType, token.AccessToken)
if accessTokenRevokeError != nil {
ctx.Logger().Errorf("%v", accessTokenRevokeError)
}
}

if c.RefreshToken != "" {
refreshTokenRevokeError = f.revokeTokenType(authConfig, refreshTokenType, c.RefreshToken)
if token.RefreshToken != "" {
refreshTokenRevokeError = f.revokeTokenType(authConfig, refreshTokenType, token.RefreshToken)
if refreshTokenRevokeError != nil {
ctx.Logger().Errorf("%v", refreshTokenRevokeError)
}
Expand All @@ -173,6 +173,12 @@ func (f *grantLogoutFilter) Request(ctx filters.FilterContext) {
}

func (f *grantLogoutFilter) Response(ctx filters.FilterContext) {
deleteCookie := createDeleteCookie(f.config, ctx.Request().Host)
ctx.Response().Header.Add("Set-Cookie", deleteCookie.String())
cookies, err := f.config.GrantCookieEncoder.Update(f.config, ctx.Request(), nil)
if err != nil {
ctx.Logger().Errorf("Failed to delete cookies: %v.", err)
return
}
for _, c := range cookies {
ctx.Response().Header.Add("Set-Cookie", c.String())
}
}

0 comments on commit e42a740

Please sign in to comment.