Compare commits

...

2 commits

Author SHA1 Message Date
a9028e7f45
feat: add session-based storage 2025-05-12 00:41:56 +02:00
56262a1d18
chore: update dependencies 2025-05-10 21:08:23 +02:00
9 changed files with 134 additions and 100 deletions

23
go.mod
View file

@ -1,22 +1,25 @@
module github.com/StiviiK/keycloak-traefik-forward-auth
go 1.21
go 1.24
toolchain go1.22.2
toolchain go1.24.2
require (
github.com/caarlos0/env v3.5.0+incompatible
github.com/coreos/go-oidc/v3 v3.12.0
github.com/coreos/go-oidc/v3 v3.14.1
github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3
golang.org/x/oauth2 v0.25.0
golang.org/x/oauth2 v0.30.0
)
require github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
require (
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/tg123/go-htpasswd v1.2.3
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/sys v0.29.0 // indirect
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
github.com/pzentenoe/go-cache v1.0.0 // indirect
)
require (
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/tg123/go-htpasswd v1.2.4
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)

38
go.sum
View file

@ -1,35 +1,37 @@
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pzentenoe/go-cache v1.0.0 h1:6jHsrh4CGKSBBmvNrEDn+EN9cJd4qOqLsHb7xWWEPBM=
github.com/pzentenoe/go-cache v1.0.0/go.mod h1:1JaNc73+p1tmcbNJwK55vtPR40h0hIoqqjlnhBZevBw=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tg123/go-htpasswd v1.2.3 h1:ALR6ZBIc2m9u70m+eAWUFt5p43ISbIvAvRFYzZPTOY8=
github.com/tg123/go-htpasswd v1.2.3/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -44,15 +44,22 @@ func (fw *ForwardAuth) IsAuthenticated(context context.Context, logger *logrus.E
var claims Claims
logger = logger.WithField("FunctionSource", "IsAuthenticated")
// Check if we have an Auth cookie
cookie, err := fw.GetAuthCookie(r)
// Check if we have a session cookie
cookie, err := fw.GetSessionCookie(r, options)
if err != nil {
logger.Error(err.Error())
return &claims, err
}
sessionId := cookie.Value
session := fw.SessionCache.Get(sessionId)
if session == nil {
err = errors.New("session not found")
return &claims, err
}
// check if the token is valid
idToken, err := fw.OidcVefifier.Verify(context, cookie.Value)
idToken, err := fw.OidcVefifier.Verify(context, session.IDToken)
switch {
case err == nil: // Token is valid
@ -65,36 +72,24 @@ func (fw *ForwardAuth) IsAuthenticated(context context.Context, logger *logrus.E
}
return &claims, nil
// Todo: Updating the cookies does sadly not work here
case strings.Contains(err.Error(), "expired"): // Token is expired
logger.Info("Received expired token, trying to refesh it.")
refreshCookie, err := fw.GetRefreshAuthCookie(r)
result, err := fw.RefreshToken(context, session.RefreshToken)
if err != nil {
fw.SessionCache.Delete(sessionId)
logger.Error(err.Error())
return &claims, err
}
result, err := fw.RefreshToken(context, refreshCookie.Value)
if err != nil {
logger.Error(err.Error())
return &claims, err
}
http.SetCookie(w, fw.MakeAuthCookie(options, result))
if len(result.RefreshToken) > 0 { // Do we have an refresh token?
http.SetCookie(w, fw.MakeRefreshAuthCookie(options, result))
}
newSession := SessionCacheItem{IDToken: result.IDToken, RefreshToken: result.RefreshToken}
fw.SessionCache.Update(sessionId, &newSession)
return result.IDTokenClaims, nil
case err != nil: // Other error
default: // Other error
fw.SessionCache.Delete(sessionId)
logger.Error(err.Error())
return &claims, err
default:
logger.Error("default case, should not happen")
return &claims, errors.New("default case")
}
}

View file

@ -26,15 +26,15 @@ func getBaseCookie(options *options.Options) *http.Cookie {
func (fw *ForwardAuth) MakeCSRFCookie(w http.ResponseWriter, r *http.Request, options *options.Options, state string) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth_csrf"
cookie.Name = options.CookiePrefix + "csrf"
cookie.Value = fmt.Sprintf("%s|%s", fw.GetReturnUri(r), state)
cookie.Expires = time.Now().Local().Add(time.Hour)
return cookie
}
func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request) (state string, redirect string, error error) {
csrfCookie, err := r.Cookie("__auth_csrf")
func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request, options *options.Options) (state string, redirect string, error error) {
csrfCookie, err := r.Cookie(options.CookiePrefix + "csrf")
if err != nil {
return "", "", errors.New("Missing csrf cookie")
}
@ -58,49 +58,28 @@ func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request) (state string, redire
func (fw *ForwardAuth) ClearCSRFCookie(options *options.Options) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth_csrf"
cookie.Name = options.CookiePrefix + "csrf"
cookie.Expires = time.Now().Local().Add(time.Hour * -1)
return cookie
}
func (fw *ForwardAuth) MakeAuthCookie(options *options.Options, authResult *AuthenticatationResult) *http.Cookie {
func (fw *ForwardAuth) MakeSessionCookie(options *options.Options, sessionId string) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth"
cookie.Value = authResult.IDToken
cookie.Expires = time.Now().Local().Add(time.Hour * 24)
cookie.Name = options.CookiePrefix + "session"
cookie.Value = sessionId
cookie.Expires = time.Now().Local().Add(time.Hour * time.Duration(options.SessionLifetime))
return cookie
}
func (fw *ForwardAuth) GetAuthCookie(r *http.Request) (*http.Cookie, error) {
return r.Cookie("__auth")
func (fw *ForwardAuth) GetSessionCookie(r *http.Request, options *options.Options) (*http.Cookie, error) {
return r.Cookie(options.CookiePrefix + "session")
}
func (fw *ForwardAuth) ClearAuthCookie(options *options.Options) *http.Cookie {
func (fw *ForwardAuth) ClearSessionCookie(options *options.Options) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth"
cookie.Expires = time.Now().Local().Add(time.Hour * -1)
return cookie
}
func (fw *ForwardAuth) MakeRefreshAuthCookie(options *options.Options, authResult *AuthenticatationResult) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth_refresh"
cookie.Value = authResult.RefreshToken
cookie.Expires = time.Now().Local().Add(time.Hour * 24)
return cookie
}
func (fw *ForwardAuth) GetRefreshAuthCookie(r *http.Request) (*http.Cookie, error) {
return r.Cookie("__auth_refresh")
}
func (fw *ForwardAuth) ClearRefreshAuthCookie(options *options.Options) *http.Cookie {
cookie := getBaseCookie(options)
cookie.Name = "__auth_refresh"
cookie.Name = options.CookiePrefix + "session"
cookie.Expires = time.Now().Local().Add(time.Hour * -1)
return cookie

View file

@ -20,6 +20,7 @@ type ForwardAuth struct {
OidcProvider *oidc.Provider
OAuth2Config oauth2.Config
OidcVefifier *oidc.IDTokenVerifier
SessionCache SessionCache
}
// Claims represents the claims struct which we get from the identity provider
@ -68,5 +69,6 @@ func Create(ctx context.Context, options *options.Options) (*ForwardAuth, error)
Scopes: scopes,
},
OidcVefifier: verifier,
SessionCache: newSessionCache(options),
}, nil
}

View file

@ -0,0 +1,51 @@
package forwardauth
import (
"time"
"github.com/StiviiK/keycloak-traefik-forward-auth/pkg/options"
"github.com/google/uuid"
"github.com/pzentenoe/go-cache"
)
type SessionCache struct {
internal *cache.Cache
}
type SessionCacheItem struct {
IDToken string
RefreshToken string
}
func newSessionCache(options *options.Options) SessionCache {
return SessionCache{
internal: cache.New(time.Hour*time.Duration(options.SessionLifetime), time.Hour),
}
}
func (c *SessionCache) Get(sessionId string) *SessionCacheItem {
itm, _ := c.internal.Get(sessionId)
if itm == nil {
return nil
}
return itm.(*SessionCacheItem)
}
func (c *SessionCache) Create(session *SessionCacheItem) string {
sessionId := uuid.New().String()
c.internal.SetDefault(sessionId, session)
return sessionId
}
func (c *SessionCache) Update(sessionId string, session *SessionCacheItem) {
_, exp, found := c.internal.GetWithExpiration(sessionId)
if found {
c.internal.Set(sessionId, session, exp.Sub(time.Now()))
} else {
c.internal.SetDefault(sessionId, session)
}
}
func (c *SessionCache) Delete(sessionId string) {
c.internal.Delete(sessionId)
}

View file

@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"github.com/StiviiK/keycloak-traefik-forward-auth/pkg/forwardauth"
"github.com/sirupsen/logrus"
)
@ -19,7 +20,7 @@ func (root *HttpHandler) callbackHandler(w http.ResponseWriter, r *http.Request,
})
// check for the csrf cookie
state, redirect, err := root.forwardAuth.ValidateCSRFCookie(r)
state, redirect, err := root.forwardAuth.ValidateCSRFCookie(r, root.options)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@ -41,9 +42,9 @@ func (root *HttpHandler) callbackHandler(w http.ResponseWriter, r *http.Request,
// clear the csrf cookie
http.SetCookie(w, root.forwardAuth.ClearCSRFCookie(root.options))
http.SetCookie(w, root.forwardAuth.MakeAuthCookie(root.options, authResult))
//if len(authResult.RefreshToken) > 0 { // Do we have an refresh token?
// http.SetCookie(w, root.forwardAuth.MakeRefreshAuthCookie(root.options, authResult))
//}
newSession := forwardauth.SessionCacheItem{IDToken: authResult.IDToken, RefreshToken: authResult.RefreshToken}
sessionId := root.forwardAuth.SessionCache.Create(&newSession)
http.SetCookie(w, root.forwardAuth.MakeSessionCookie(root.options, sessionId))
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
}

View file

@ -38,8 +38,7 @@ func (root *HttpHandler) rootHandler(w http.ResponseWriter, r *http.Request, for
logger = logger.WithField("FunctionSource", "RootHandler")
logger.Warn("IsAuthenticated failed, initating login flow.")
http.SetCookie(w, root.forwardAuth.ClearAuthCookie(root.options))
//http.SetCookie(w, root.forwardAuth.ClearRefreshAuthCookie(root.options))
http.SetCookie(w, root.forwardAuth.ClearSessionCookie(root.options))
state := uuid.New().String()
http.SetCookie(w, root.forwardAuth.MakeCSRFCookie(w, r, root.options, state))

View file

@ -18,12 +18,14 @@ type Options struct {
ClientSecret string `env:"CLIENT_SECRET"`
AuthDomain string `env:"AUTH_DOMAIN"`
CookieDomain string `env:"COOKIE_DOMAIN"`
CookiePrefix string `env:"COOKIE_PREFIX" envDefault:"oidca_"`
Port int `env:"PORT" envDefault:"4181"`
RedirectURL string `env:"REDIRECT_URL" envDefault:"/auth/resp"`
Scopes string `env:"SCOPES"`
BypassUser string `env:"BYPASS_USER"`
BypassFile string `env:"BYPASS_FILE"`
BypassPwd *htpasswd.File
SessionLifetime int `env:"SESSION_LIFETIME" envDefault:"24"`
}
// LoadOptions parses the environment vars and the options