diff --git a/.gitignore b/.gitignore index 57f3044462..393f27d53d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ _testmain.go # vi Dockerfile.dev # docker build -f Dockerfile.dev . Dockerfile.dev + +obj \ No newline at end of file diff --git a/Makefile b/Makefile index fdfda8eafd..e373766e95 100644 --- a/Makefile +++ b/Makefile @@ -116,3 +116,7 @@ validate-go-version: .PHONY: local-env-% local-env-%: make -C contrib/local-environment $* + +.PHONY: local-debug-build +local-debug-build: + go build -gcflags="all=-N -l" -o app.exe \ No newline at end of file diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index 24e6a429a1..03f10faa17 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -388,6 +388,8 @@ character. | `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID
default set to 'email' | | `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id
By default `aud` claim is used for verification. | | `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed
to pass verification in addition to the client id. | +| `enableCookieRefresh` | _bool_ | Enable cookie refresh functionality that is going to be triggered every time the session is updated | +| `cookieRefreshName` | _string_ | Name of the cookie that is going to be extracted from the request and refreshed | ### Provider diff --git a/main_test.go b/main_test.go index 6a640817b7..5bce633546 100644 --- a/main_test.go +++ b/main_test.go @@ -77,6 +77,7 @@ providers: insecureSkipNonce: true audienceClaims: [aud] extraAudiences: [] + cookieRefreshName: 'hsdpamcookie' loginURLParameters: - name: approval_prompt default: @@ -157,6 +158,7 @@ redirect_url="http://localhost:4180/oauth2/callback" AudienceClaims: []string{"aud"}, ExtraAudiences: []string{}, InsecureSkipNonce: true, + CookieRefreshName: "hsdpamcookie", }, LoginURLParameters: []options.LoginURLParameter{ {Name: "approval_prompt", Default: []string{"force"}}, diff --git a/oauthproxy.go b/oauthproxy.go index d11040c380..4eb58e0ccc 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -393,6 +393,12 @@ func buildSessionChain(opts *options.Options, provider providers.Provider, sessi ValidateSession: provider.ValidateSession, })) + oidcProviderSettings := opts.Providers[0].OIDCConfig + if oidcProviderSettings.EnableCookieRefresh { + chain = chain.Append(middleware.NewCookieRefresh(&middleware.CookieRefreshOptions{IssuerURL: oidcProviderSettings.IssuerURL, CookieRefreshName: oidcProviderSettings.CookieRefreshName})) + logger.Printf("Enabling OIDC cookie refresh for the cookie '%s' functionality because OIDCEnableCookieRefresh is enabled", oidcProviderSettings.CookieRefreshName) + } + return chain } diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 616b33abcb..c3c010a289 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -543,6 +543,8 @@ type LegacyProvider struct { OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` + OIDCEnableCookieRefresh bool `flag:"oidc-enable-cookie-refresh" cfg:"oidc_enable_cookie_refresh"` + OIDCCookieRefreshName string `flag:"oidc-cookie-refresh-name" cfg:"oidc_cookie_refresh_name"` LoginURL string `flag:"login-url" cfg:"login_url"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` ProfileURL string `flag:"profile-url" cfg:"profile_url"` @@ -601,6 +603,8 @@ func legacyProviderFlagSet() *pflag.FlagSet { flagSet.String("oidc-email-claim", OIDCEmailClaim, "which OIDC claim contains the user's email") flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id") flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification") + flagSet.Bool("oidc-enable-cookie-refresh", false, "Refresh the OIDC provider cookies to enable SSO in an extended period of time") + flagSet.String("oidc-cookie-refresh-name", "hsdpamcookie", "The name of the cookie that the OIDC provider uses to keep its session fresh") flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("profile-url", "", "Profile access endpoint") @@ -702,6 +706,8 @@ func (l *LegacyProvider) convert() (Providers, error) { GroupsClaim: l.OIDCGroupsClaim, AudienceClaims: l.OIDCAudienceClaims, ExtraAudiences: l.OIDCExtraAudiences, + EnableCookieRefresh: l.OIDCEnableCookieRefresh, + CookieRefreshName: l.OIDCCookieRefreshName, } // Support for legacy configuration option diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go index cface5140b..e5d9097816 100644 --- a/pkg/apis/options/load_test.go +++ b/pkg/apis/options/load_test.go @@ -44,6 +44,7 @@ var _ = Describe("Load", func() { OIDCGroupsClaim: "groups", OIDCAudienceClaims: []string{"aud"}, InsecureOIDCSkipNonce: true, + OIDCCookieRefreshName: "hsdpamcookie", }, Options: Options{ diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 7b9934051c..a7e02b7d17 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -230,6 +230,10 @@ type OIDCOptions struct { // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. ExtraAudiences []string `json:"extraAudiences,omitempty"` + // Enable cookie refresh functionality that is going to be triggered every time the session is updated + EnableCookieRefresh bool `json:"enableCookieRefresh,omitempty"` + // Name of the cookie that is going to be extracted from the request and refreshed + CookieRefreshName string `json:"cookieRefreshName,omitempty"` } type LoginGovOptions struct { diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 2fd5161347..43075adcd7 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -31,8 +31,9 @@ type SessionState struct { IntrospectClaims string `msgpack:"ic,omitempty"` // Internal helpers, not serialized - Clock clock.Clock `msgpack:"-"` - Lock Lock `msgpack:"-"` + Clock clock.Clock `msgpack:"-"` + Lock Lock `msgpack:"-"` + SessionJustRefreshed bool `msgpack:"-"` } func (s *SessionState) ObtainLock(ctx context.Context, expiration time.Duration) error { diff --git a/pkg/middleware/cookie_refresh.go b/pkg/middleware/cookie_refresh.go new file mode 100644 index 0000000000..a64c161f15 --- /dev/null +++ b/pkg/middleware/cookie_refresh.go @@ -0,0 +1,62 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/justinas/alice" + middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" +) + +type CookieRefreshOptions struct { + IssuerURL string + CookieRefreshName string +} + +func NewCookieRefresh(opts *CookieRefreshOptions) alice.Constructor { + cr := &cookieRefresh{ + HTTPClient: &http.Client{}, + IssuerURL: opts.IssuerURL, + CookieRefreshName: opts.CookieRefreshName, + } + return cr.refreshCookie +} + +type cookieRefresh struct { + HTTPClient *http.Client + IssuerURL string + CookieRefreshName string +} + +func (cr *cookieRefresh) refreshCookie(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + scope := middlewareapi.GetRequestScope(req) + if scope.Session == nil || !scope.Session.SessionJustRefreshed { + next.ServeHTTP(rw, req) + return + } + + cookie, err := req.Cookie(cr.CookieRefreshName) + if err != nil { + logger.Errorf("SSO Cookie Refresher - Could find '%s' cookie in the request: %v", cr.CookieRefreshName, err) + return + } + resp := requests.New(fmt.Sprintf("%s/session/refresh", cr.IssuerURL)). + WithContext(req.Context()). + WithMethod("GET"). + SetHeader("api-version", "1"). + SetHeader("Cookie", fmt.Sprintf("%s=%s", cr.CookieRefreshName, cookie.Value)). + Do() + + if resp.StatusCode() != http.StatusNoContent { + bodyString := string(resp.Body()) + logger.Errorf("SSO Cookie Refresher - Could not refresh the '%s' cookie due to status and content: %v - %v", cr.CookieRefreshName, resp.StatusCode(), bodyString) + return + } + + logger.Printf("SSO Cookie Refresher - Cookie '%s' refreshed", cr.CookieRefreshName) + next.ServeHTTP(rw, req) + }) +} diff --git a/providers/oidc.go b/providers/oidc.go index de7b827754..f55ec0e4fc 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -159,6 +159,7 @@ func (p *OIDCProvider) RefreshSession(ctx context.Context, s *sessions.SessionSt if err != nil { return false, fmt.Errorf("unable to redeem refresh token: %v", err) } + s.SessionJustRefreshed = true return true, nil }