Parcourir la source

webapi: make logout flow work correctly

Mike il y a 1 semaine
Parent
commit
8f9e9662ba

+ 19 - 0
server/webapi/handlers/auth.go

@@ -187,6 +187,25 @@ func (h *AuthHandler) loginRedirectURL(r *http.Request) string {
 	return fmt.Sprintf("%s://%s/_cqr/login/login.psp", scheme, r.Host)
 }
 
+func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
+	h.clearLoginPSPCookies(w)
+
+	loginURL := h.loginRedirectURL(r)
+	q := url.Values{}
+	if devID := r.URL.Query().Get("devId"); devID != "" {
+		q.Set("devId", devID)
+	}
+	if succURL := r.URL.Query().Get("succUrl"); succURL != "" {
+		q.Set("succUrl", succURL)
+	}
+	if enc := q.Encode(); enc != "" {
+		loginURL += "?" + enc
+	}
+
+	h.Logger.InfoContext(r.Context(), "logout", "devId", r.URL.Query().Get("devId"))
+	http.Redirect(w, r, loginURL, http.StatusFound)
+}
+
 // ClientLogin handles POST /auth/clientLogin requests.
 // This endpoint authenticates users and returns an authentication token.
 func (h *AuthHandler) ClientLogin(w http.ResponseWriter, r *http.Request) {

+ 16 - 0
server/webapi/handlers/login_psp.go

@@ -161,6 +161,22 @@ func (h *AuthHandler) setLoginPSPCookies(w http.ResponseWriter, screenName state
 	http.SetCookie(w, cookie("localAuthUser", loginID+"||"+loginID))
 }
 
+// clearLoginPSPCookies expires the SSO cookies set by setLoginPSPCookies (plus
+// the oldAimToken cookie honored by getToken) so the browser is logged out.
+func (h *AuthHandler) clearLoginPSPCookies(w http.ResponseWriter) {
+	for _, name := range []string{"RSP_USER", "RSP_LOCAL", "localAuthUser", "oldAimToken"} {
+		http.SetCookie(w, &http.Cookie{
+			Name:     name,
+			Value:    "",
+			Path:     "/",
+			Expires:  time.Unix(0, 0),
+			MaxAge:   -1,
+			HttpOnly: false,
+			SameSite: http.SameSiteLaxMode,
+		})
+	}
+}
+
 func defaultLoginSuccURL(r *http.Request) string {
 	scheme := "http"
 	if r.TLS != nil {

+ 28 - 0
server/webapi/handlers/login_psp_test.go

@@ -28,6 +28,34 @@ func TestAuthHandler_LoginPSP_GET(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), `name="devId" value="dev1"`)
 }
 
+func TestAuthHandler_Logout(t *testing.T) {
+	handler := &AuthHandler{Logger: slog.Default()}
+
+	req := httptest.NewRequest(http.MethodGet, "/auth/logout?f=json&a=sometoken&devId=dev1&succUrl=http%3A%2F%2Flocalhost%3A8000%2F.client%2F", nil)
+	rr := httptest.NewRecorder()
+
+	handler.Logout(rr, req)
+
+	assert.Equal(t, http.StatusFound, rr.Code)
+
+	loc, err := url.Parse(rr.Header().Get("Location"))
+	assert.NoError(t, err)
+	assert.Equal(t, "/_cqr/login/login.psp", loc.Path)
+	assert.Equal(t, "dev1", loc.Query().Get("devId"))
+	assert.Equal(t, "http://localhost:8000/.client/", loc.Query().Get("succUrl"))
+
+	// SSO cookies are expired so the browser is logged out.
+	cleared := map[string]bool{}
+	for _, c := range rr.Result().Cookies() {
+		if c.MaxAge < 0 {
+			cleared[c.Name] = true
+		}
+	}
+	for _, name := range []string{"RSP_USER", "RSP_LOCAL", "localAuthUser", "oldAimToken"} {
+		assert.True(t, cleared[name], "expected %s cookie to be cleared", name)
+	}
+}
+
 func TestAuthHandler_LoginPSP_POST_Success(t *testing.T) {
 	handler := &AuthHandler{
 		AuthService: &testAuthService{

+ 4 - 0
server/webapi/server.go

@@ -119,6 +119,10 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 			w.WriteHeader(http.StatusNoContent)
 		})
 
+		// Web AIM navigates the browser here on File > Logout; clear SSO state
+		// and redirect to the login screen.
+		mux.HandleFunc("GET /auth/logout", authHandler.Logout)
+
 		mux.HandleFunc("GET /_cqr/login/login.psp", authHandler.LoginPSP)
 		mux.HandleFunc("POST /_cqr/login/login.psp", authHandler.LoginPSP)