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.

Another implementation may encode token value into multiple cookies.

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
  • Loading branch information
AlexanderYastrebov committed Mar 20, 2024
1 parent 5c2e09c commit 2a860d2
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 74 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(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(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(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{config: c}
}

c.initialized = true
return nil
}
Expand Down
47 changes: 42 additions & 5 deletions filters/auth/grantcookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@ 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(request *http.Request, token *oauth2.Token) ([]*http.Cookie, error)

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

type EncryptedCookieEncoder struct {
config *OAuthConfig
}

var _ CookieEncoder = &EncryptedCookieEncoder{}

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

func (ce *EncryptedCookieEncoder) Read(request *http.Request) (*oauth2.Token, error) {
c, err := extractCookie(request, ce.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 +81,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(&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(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(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())
}
}
78 changes: 47 additions & 31 deletions skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,10 @@ type Options struct {
// OAuth2GrantInsecure omits Secure attribute of the token cookie and uses http scheme for callback url.
OAuth2GrantInsecure bool

// OAuthGrantConfig specifies configuration for OAuth grant flow.
// A new instance will be created from OAuth* options when not specified.
OAuthGrantConfig *auth.OAuthConfig

// CompressEncodings, if not empty replace default compression encodings
CompressEncodings []string

Expand Down Expand Up @@ -957,6 +961,33 @@ func (o *Options) KubernetesDataClientOptions() kubernetes.Options {
}
}

func (o *Options) OAuthGrantOptions() *auth.OAuthConfig {
oauthConfig := &auth.OAuthConfig{}

oauthConfig.AuthURL = o.OAuth2AuthURL
oauthConfig.TokenURL = o.OAuth2TokenURL
oauthConfig.RevokeTokenURL = o.OAuth2RevokeTokenURL
oauthConfig.TokeninfoURL = o.OAuthTokeninfoURL
oauthConfig.SecretFile = o.OAuth2SecretFile
oauthConfig.ClientID = o.OAuth2ClientID
oauthConfig.ClientSecret = o.OAuth2ClientSecret
oauthConfig.ClientIDFile = o.OAuth2ClientIDFile
oauthConfig.ClientSecretFile = o.OAuth2ClientSecretFile
oauthConfig.CallbackPath = o.OAuth2CallbackPath
oauthConfig.AuthURLParameters = o.OAuth2AuthURLParameters
oauthConfig.Secrets = o.SecretsRegistry
oauthConfig.AccessTokenHeaderName = o.OAuth2AccessTokenHeaderName
oauthConfig.TokeninfoSubjectKey = o.OAuth2TokeninfoSubjectKey
oauthConfig.GrantTokeninfoKeys = o.OAuth2GrantTokeninfoKeys
oauthConfig.TokenCookieName = o.OAuth2TokenCookieName
oauthConfig.TokenCookieRemoveSubdomains = &o.OAuth2TokenCookieRemoveSubdomains
oauthConfig.Insecure = o.OAuth2GrantInsecure
oauthConfig.ConnectionTimeout = o.OAuthTokeninfoTimeout
oauthConfig.MaxIdleConnectionsPerHost = o.IdleConnectionsPerHost

return oauthConfig
}

type serverErrorLogWriter struct{}

func (*serverErrorLogWriter) Write(p []byte) (int, error) {
Expand Down Expand Up @@ -1772,37 +1803,22 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
o.TLSMinVersion = tls.VersionTLS12
}

oauthConfig := &auth.OAuthConfig{}
if o.EnableOAuth2GrantFlow /* explicitly enable grant flow */ {
grantSecrets := secrets.NewSecretPaths(o.CredentialsUpdateInterval)
defer grantSecrets.Close()

oauthConfig.AuthURL = o.OAuth2AuthURL
oauthConfig.TokenURL = o.OAuth2TokenURL
oauthConfig.RevokeTokenURL = o.OAuth2RevokeTokenURL
oauthConfig.TokeninfoURL = o.OAuthTokeninfoURL
oauthConfig.SecretFile = o.OAuth2SecretFile
oauthConfig.ClientID = o.OAuth2ClientID
oauthConfig.ClientSecret = o.OAuth2ClientSecret
oauthConfig.ClientIDFile = o.OAuth2ClientIDFile
oauthConfig.ClientSecretFile = o.OAuth2ClientSecretFile
oauthConfig.CallbackPath = o.OAuth2CallbackPath
oauthConfig.AuthURLParameters = o.OAuth2AuthURLParameters
oauthConfig.SecretsProvider = grantSecrets
oauthConfig.Secrets = o.SecretsRegistry
oauthConfig.AccessTokenHeaderName = o.OAuth2AccessTokenHeaderName
oauthConfig.TokeninfoSubjectKey = o.OAuth2TokeninfoSubjectKey
oauthConfig.GrantTokeninfoKeys = o.OAuth2GrantTokeninfoKeys
oauthConfig.TokenCookieName = o.OAuth2TokenCookieName
oauthConfig.TokenCookieRemoveSubdomains = &o.OAuth2TokenCookieRemoveSubdomains
oauthConfig.Insecure = o.OAuth2GrantInsecure
oauthConfig.ConnectionTimeout = o.OAuthTokeninfoTimeout
oauthConfig.MaxIdleConnectionsPerHost = o.IdleConnectionsPerHost
oauthConfig.Tracer = tracer

if err := oauthConfig.Init(); err != nil {
log.Errorf("Failed to initialize oauth grant filter: %v.", err)
return err
oauthConfig := o.OAuthGrantConfig
if oauthConfig == nil {
oauthConfig = o.OAuthGrantOptions()
o.OAuthGrantConfig = oauthConfig

grantSecrets := secrets.NewSecretPaths(o.CredentialsUpdateInterval)
defer grantSecrets.Close()

oauthConfig.SecretsProvider = grantSecrets
oauthConfig.Tracer = tracer

if err := oauthConfig.Init(); err != nil {
log.Errorf("Failed to initialize oauth grant filter: %v.", err)
return err
}
}

o.CustomFilters = append(o.CustomFilters,
Expand Down Expand Up @@ -1972,7 +1988,7 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
ro.PreProcessors = append(ro.PreProcessors, schedulerRegistry.PreProcessor())

if o.EnableOAuth2GrantFlow /* explicitly enable grant flow when callback route was not disabled */ {
ro.PreProcessors = append(ro.PreProcessors, oauthConfig.NewGrantPreprocessor())
ro.PreProcessors = append(ro.PreProcessors, o.OAuthGrantConfig.NewGrantPreprocessor())
}

if o.EnableOpenPolicyAgent {
Expand Down

0 comments on commit 2a860d2

Please sign in to comment.