mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-23 21:16:14 +00:00
feat: add handler for forward-auth
This commit is contained in:
@@ -47,6 +47,7 @@ The following flags are available:
|
||||
| `redis.tls` | boolean | `true` | Whether or not to use TLS for connecting to Redis. Does not override TLS config set by `redis.uri`. |
|
||||
| `redis.uri` | string | | Redis URI string. An empty value will fall back to `redis-address`. |
|
||||
| `redis.username` | string | | Username for Redis. Overrides username set by `redis.uri`. |
|
||||
| `session.forward-auth` | boolean | `false` | Enable endpoint for forward authentication. |
|
||||
| `session.inactivity` | boolean | `false` | Automatically expire user sessions if they have not refreshed their tokens within a given duration. |
|
||||
| `session.inactivity-timeout` | duration | `30m` | Inactivity timeout for user sessions. |
|
||||
| `session.max-lifetime` | duration | `10h` | Max lifetime for user sessions. |
|
||||
|
||||
+33
-7
@@ -6,13 +6,14 @@ Wonderwall exposes and owns these endpoints (which means they will never be prox
|
||||
|
||||
Endpoints that are available for use by applications:
|
||||
|
||||
| Path | Description | Notes |
|
||||
|--------------------------------|----------------------------------------------------------------------|---------------------------------------------------|
|
||||
| `GET /oauth2/login` | Initiates the OpenID Connect Authorization Code flow | |
|
||||
| `GET /oauth2/logout` | Performs local logout and redirects the user to global/single-logout | |
|
||||
| `GET /oauth2/logout/local` | Performs local logout only | Disabled when `openid.provider` is `idporten`. |
|
||||
| `GET /oauth2/session` | Returns the current user's session metadata | |
|
||||
| `POST /oauth2/session/refresh` | Refreshes the tokens for the user's session. | Requires the `session.refresh` flag to be enabled |
|
||||
| Path | Description | Notes |
|
||||
|-----------------------------------|----------------------------------------------------------------------|---------------------------------------------------|
|
||||
| `GET /oauth2/login` | Initiates the OpenID Connect Authorization Code flow | |
|
||||
| `GET /oauth2/logout` | Performs local logout and redirects the user to global/single-logout | |
|
||||
| `GET /oauth2/logout/local` | Performs local logout only | Disabled when `openid.provider` is `idporten`. |
|
||||
| `GET /oauth2/session` | Returns the current user's session metadata | |
|
||||
| `POST /oauth2/session/refresh` | Refreshes the tokens for the user's session. | Requires the `session.refresh` flag to be enabled |
|
||||
| `GET /oauth2/session/forwardauth` | Checks the user's session and refreshes it, if necessary. | |
|
||||
|
||||
## Endpoints for Identity Providers
|
||||
|
||||
@@ -232,3 +233,28 @@ of the tokens returned by the identity provider.
|
||||
The cooldown period exists to limit the amount of refresh token requests that we send to the identity provider.
|
||||
|
||||
A refresh is only triggered if `tokens.refresh_cooldown` is `false`. Requests to the endpoint are idempotent while the cooldown is active.
|
||||
|
||||
---
|
||||
|
||||
### `/oauth2/session/forwardauth`
|
||||
|
||||
This endpoint only exists if the `session.forward-auth` flag is enabled.
|
||||
|
||||
The endpoint is intended for use in forward authentication scenarios, where a reverse proxy delegates authentication checks to Wonderwall.
|
||||
The user's session is checked and refreshed, if necessary.
|
||||
|
||||
#### Request:
|
||||
|
||||
```
|
||||
GET /oauth2/session/forwardauth
|
||||
```
|
||||
|
||||
#### Response:
|
||||
|
||||
```
|
||||
HTTP/2 204 No Content
|
||||
```
|
||||
|
||||
The endpoint responds with a `HTTP 204 No Content` if the session is valid.
|
||||
|
||||
If the session is invalid (i.e. expired, inactive, or not found), the response is an `HTTP 401 Unauthorized`.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ForwardAuth bool `json:"forward-auth"`
|
||||
Inactivity bool `json:"inactivity"`
|
||||
InactivityTimeout time.Duration `json:"inactivity-timeout"`
|
||||
MaxLifetime time.Duration `json:"max-lifetime"`
|
||||
@@ -28,6 +29,7 @@ func (s Session) Validate() error {
|
||||
}
|
||||
|
||||
const (
|
||||
SessionForwardAuth = "session.forward-auth"
|
||||
SessionInactivity = "session.inactivity"
|
||||
SessionInactivityTimeout = "session.inactivity-timeout"
|
||||
SessionMaxLifetime = "session.max-lifetime"
|
||||
@@ -36,6 +38,7 @@ const (
|
||||
)
|
||||
|
||||
func sessionFlags() {
|
||||
flag.Bool(SessionForwardAuth, false, "Enable endpoint for forward authentication.")
|
||||
flag.Bool(SessionInactivity, false, "Automatically expire user sessions if they have not refreshed their tokens within a given duration.")
|
||||
flag.Duration(SessionInactivityTimeout, 30*time.Minute, "Inactivity timeout for user sessions.")
|
||||
flag.Duration(SessionMaxLifetime, 10*time.Hour, "Max lifetime for user sessions.")
|
||||
|
||||
@@ -440,6 +440,29 @@ func (s *Standalone) sessionWriteMetadataResponse(w http.ResponseWriter, r *http
|
||||
return json.NewEncoder(w).Encode(metadata)
|
||||
}
|
||||
|
||||
func (s *Standalone) SessionForwardAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.Config.Session.ForwardAuth {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := s.SessionManager.GetOrRefresh(r)
|
||||
if err != nil {
|
||||
logger := mw.LogEntryFrom(r)
|
||||
if errors.Is(err, session.ErrInvalidExternal) || errors.Is(err, session.ErrInvalid) || errors.Is(err, session.ErrNotFound) {
|
||||
logger.Infof("session/forwardauth: %+v", err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Warnf("session/forwardauth: %+v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Wildcard proxies all requests to an upstream server.
|
||||
func (s *Standalone) Wildcard(w http.ResponseWriter, r *http.Request) {
|
||||
s.UpstreamProxy.Handler(s, w, r)
|
||||
|
||||
@@ -190,6 +190,12 @@ func (s *SSOProxy) SessionRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *SSOProxy) SessionForwardAuth(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = paths.OAuth2 + paths.Session + paths.ForwardAuth
|
||||
removeMiddlewareHeaders(w)
|
||||
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Wildcard proxies all requests to an upstream server.
|
||||
func (s *SSOProxy) Wildcard(w http.ResponseWriter, r *http.Request) {
|
||||
s.UpstreamProxy.Handler(s, w, r)
|
||||
|
||||
@@ -521,6 +521,33 @@ func TestSession_WithRefreshAuto(t *testing.T) {
|
||||
assert.Greater(t, data.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
}
|
||||
|
||||
func TestSessionForwardAuth(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.ForwardAuth = true
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
noSessionResp := sessionForwardAuth(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusUnauthorized, noSessionResp.StatusCode)
|
||||
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionForwardAuth(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestSessionForwardAuth_Disabled(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.ForwardAuth = false
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
noSessionResp := sessionForwardAuth(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusNotFound, noSessionResp.StatusCode)
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
@@ -693,6 +720,13 @@ func sessionRefresh(t *testing.T, idp *mock.IdentityProvider, rpClient *http.Cli
|
||||
return post(t, rpClient, sessionRefreshURL.String())
|
||||
}
|
||||
|
||||
func sessionForwardAuth(t *testing.T, idp *mock.IdentityProvider, rpClient *http.Client) response {
|
||||
sessionForwardAuthURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/session/forwardauth")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return get(t, rpClient, sessionForwardAuthURL.String())
|
||||
}
|
||||
|
||||
func waitForRefreshCooldownTimer(t *testing.T, idp *mock.IdentityProvider, rpClient *http.Client) {
|
||||
timeout := time.After(5 * time.Second)
|
||||
ticker := time.Tick(500 * time.Millisecond)
|
||||
|
||||
@@ -71,15 +71,16 @@ func (m *PrometheusMiddleware) Initialize(path, method string, code int) {
|
||||
|
||||
func (m *PrometheusMiddleware) Handler(next http.Handler) http.Handler {
|
||||
relevantPaths := map[string]bool{
|
||||
paths.OAuth2 + paths.Login: true,
|
||||
paths.OAuth2 + paths.LoginCallback: true,
|
||||
paths.OAuth2 + paths.Logout: true,
|
||||
paths.OAuth2 + paths.LogoutCallback: true,
|
||||
paths.OAuth2 + paths.LogoutFrontChannel: true,
|
||||
paths.OAuth2 + paths.LogoutLocal: true,
|
||||
paths.OAuth2 + paths.Ping: false,
|
||||
paths.OAuth2 + paths.Session: true,
|
||||
paths.OAuth2 + paths.Session + paths.Refresh: true,
|
||||
paths.OAuth2 + paths.Login: true,
|
||||
paths.OAuth2 + paths.LoginCallback: true,
|
||||
paths.OAuth2 + paths.Logout: true,
|
||||
paths.OAuth2 + paths.LogoutCallback: true,
|
||||
paths.OAuth2 + paths.LogoutFrontChannel: true,
|
||||
paths.OAuth2 + paths.LogoutLocal: true,
|
||||
paths.OAuth2 + paths.Ping: false,
|
||||
paths.OAuth2 + paths.Session: true,
|
||||
paths.OAuth2 + paths.Session + paths.Refresh: true,
|
||||
paths.OAuth2 + paths.Session + paths.ForwardAuth: true,
|
||||
}
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -11,4 +11,5 @@ const (
|
||||
Ping = "/ping"
|
||||
Refresh = "/refresh"
|
||||
Session = "/session"
|
||||
ForwardAuth = "/forwardauth"
|
||||
)
|
||||
|
||||
@@ -36,8 +36,11 @@ type Handlers interface {
|
||||
LogoutLocal(http.ResponseWriter, *http.Request)
|
||||
// Session returns metadata for the current user's session.
|
||||
Session(http.ResponseWriter, *http.Request)
|
||||
// SessionRefresh refreshes current user's session and returns the associated updated metadata.
|
||||
// SessionRefresh forces a refresh of the current user's session and returns the associated updated metadata.
|
||||
SessionRefresh(http.ResponseWriter, *http.Request)
|
||||
// SessionForwardAuth checks the current user's session and refreshes it, if necessary.
|
||||
// For use in forward authentication scenarios.
|
||||
SessionForwardAuth(w http.ResponseWriter, r *http.Request)
|
||||
// Wildcard handles all requests not matching the other handlers.
|
||||
Wildcard(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
@@ -107,6 +110,7 @@ func New(src Source, cfg *config.Config) chi.Router {
|
||||
r.Get("/", src.Session)
|
||||
r.Get(paths.Refresh, src.SessionRefresh)
|
||||
r.Post(paths.Refresh, src.SessionRefresh)
|
||||
r.Get(paths.ForwardAuth, src.SessionForwardAuth)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user