diff --git a/README.md b/README.md index 21d3537b..db22f6ad 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,9 @@ 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. | +| `SCHEME_HEADER` | `` | Header to use for incoming request scheme. If ommitted or header is not present in request, SCHEME_DEFAULT will be used instead. | 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/state.go b/state.go index 8c910410..93bbec29 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,13 @@ func init() { gob.Register(State{}) } +// TODO move state code to its own package +// type Config struct { +// SchemeDefault string +// SchemeHeader string +// SessionDomain string +// } + type State struct { // FirstVisitedURL is the URL that the user visited when we redirected them // to login. @@ -32,14 +41,46 @@ 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 { + // Use header value if it exists + s := r.Header.Get(config.SchemeHeader) + if s == "" { + s = config.SchemeDefault + } + // XXX Could 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: s + "://" + 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.URL.Path) session := sessions.NewSession(store, oidcStateCookie) session.Options.MaxAge = int(20 * time.Minute) session.Values[sessionValueState] = *s