diff --git a/README.md b/README.md index 21d3537b..47b642f5 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,8 @@ Session store-related settings: | `OIDC_STATE_STORE_PATH` | "/var/lib/authservice/oidc_state.db" | Path to the session store used to save the sessions for the OIDC state parameter. | | `SESSION_MAX_AGE` | "86400" | Time in seconds after which sessions expire. Defaults to a day (24h). | | `SESSION_SAME_SITE` | "Lax" | SameSite attribute of the session cookie. Check details of SameSite attribute [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). Its value can be "None", "Lax" or "Strict". | -| `SESSION_DOMAIN` | "" | Domain attribute of the session cookie. Check details of Domain attribute [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) | +| `SESSION_DOMAIN` | "" | Domain attribute of the session cookie. Check details of Domain attribute [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie). If len(SESSION_DOMAIN) > 0 the incoming request's host and scheme are also saved in the state rather than just the path. This enables AuthService to service all subdomains of SESSION_DOMAIN. | +| `SCHEME_DEFAULT` | `https` | Default scheme for incoming requests used when the `X-Forwarded-Proto` header is not present in the incoming request. Only used if len(SESSION_DOMAIN) > 0. | By default, the AuthService keeps sessions to check if a user is authenticated. However, there may be times where we want to check a user's logged in status at the Provider, effectively making the Provider the one keeping the diff --git a/main.go b/main.go index 4bcac1c3..cba173cf 100644 --- a/main.go +++ b/main.go @@ -170,6 +170,7 @@ func main() { // TODO: Add support for Redis store: store, oidcStateStore: oidcStateStore, + stateFunc: NewStateFunc(c), afterLoginRedirectURL: c.AfterLoginURL.String(), homepageURL: c.HomepageURL.String(), afterLogoutRedirectURL: c.AfterLogoutURL.String(), diff --git a/server.go b/server.go index 2ce29814..dd877348 100644 --- a/server.go +++ b/server.go @@ -47,6 +47,7 @@ type server struct { upstreamHTTPHeaderOpts httpHeaderOpts caBundle []byte sessionSameSite http.SameSite + stateFunc StateFunc } // jwtClaimOpts specifies the location of the user's identity inside a JWT's @@ -142,7 +143,7 @@ func (s *server) authCodeFlowAuthenticationRequest(w http.ResponseWriter, r *htt logger := loggerForRequest(r) // Initiate OIDC Flow with Authorization Request. - state, err := createState(r, w, s.oidcStateStore) + state, err := createState(r, w, s.oidcStateStore, s.stateFunc) if err != nil { logger.Errorf("Failed to save state in store: %v", err) returnMessage(w, http.StatusInternalServerError, "Failed to save state in store.") diff --git a/settings.go b/settings.go index f54d5d4c..4e587322 100644 --- a/settings.go +++ b/settings.go @@ -51,6 +51,7 @@ type config struct { SessionMaxAge int `split_words:"true" default:"86400"` SessionSameSite string `split_words:"true" default:"Lax"` SessionDomain string `split_words:"true"` + SchemeDefault string `split_words:"true"` // Site ClientName string `split_words:"true" default:"AuthService"` diff --git a/state.go b/state.go index 1c6eb77e..892240bd 100644 --- a/state.go +++ b/state.go @@ -5,10 +5,12 @@ package main import ( "encoding/gob" "net/http" + "strings" "time" "github.com/gorilla/sessions" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) const ( @@ -20,6 +22,8 @@ func init() { gob.Register(State{}) } +// TODO maybe move state code to its own package + type State struct { // FirstVisitedURL is the URL that the user visited when we redirected them // to login. @@ -32,14 +36,47 @@ func newState(firstVisitedURL string) *State { } } +type StateFunc func(*http.Request) *State + +func NewStateFunc(config *config) StateFunc { + if len(config.SessionDomain) > 0 { + return newSchemeAndHost(config) + } + return relativeURL +} + +func relativeURL(r *http.Request) *State { + return &State{ + FirstVisitedURL: r.URL.String(), + } +} + +func newSchemeAndHost(config *config) StateFunc { + return func(r *http.Request) *State { + // try to get request protocol from header + p := r.Header.Get("X-Forwarded-Proto") + if p == "" { + // XXX maybe could hardcode "https" + p = config.SchemeDefault + } + // XXX maybe return an error here. would require changing the StateFunc type + if !strings.HasSuffix(r.Host, config.SessionDomain) { + log.Warnf("Request host %q is not a subdomain of %q", r.Host, config.SessionDomain) + } + return &State{ + FirstVisitedURL: p + "://" + r.Host + r.URL.String(), + } + } +} + // createState creates the state parameter from the incoming request, stores // it in the session store and sets a cookie with the session key. // It returns the session key, which can be used as the state value to start // an OIDC authentication request. func createState(r *http.Request, w http.ResponseWriter, - store sessions.Store) (string, error) { + store sessions.Store, sf StateFunc) (string, error) { - s := newState(r.URL.Path) + s := sf(r) session := sessions.NewSession(store, oidcStateCookie) session.Options.MaxAge = int(20 * time.Minute) session.Options.Path = "/"