Skip to content

Commit 74ff813

Browse files
committed
feat: add different logout behaviors
1 parent 23c8b45 commit 74ff813

File tree

3 files changed

+72
-13
lines changed

3 files changed

+72
-13
lines changed

internal/api/logout.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
11
package api
22

33
import (
4+
"fmt"
45
"net/http"
56

67
"github.com/supabase/gotrue/internal/models"
78
"github.com/supabase/gotrue/internal/storage"
89
)
910

11+
type LogoutBehavior string
12+
13+
const (
14+
LogoutGlobal LogoutBehavior = "global"
15+
LogoutLocal LogoutBehavior = "local"
16+
LogoutOthers LogoutBehavior = "others"
17+
)
18+
1019
// Logout is the endpoint for logging out a user and thereby revoking any refresh tokens
1120
func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
1221
ctx := r.Context()
1322
db := a.db.WithContext(ctx)
1423
config := a.config
1524

16-
a.clearCookieTokens(config, w)
25+
behavior := LogoutGlobal
26+
27+
switch r.URL.Query().Get("behavior") {
28+
case "", "global":
29+
behavior = LogoutGlobal
30+
31+
case "local":
32+
behavior = LogoutLocal
33+
34+
case "others":
35+
behavior = LogoutOthers
36+
37+
default:
38+
return badRequestError(fmt.Sprintf("Unsupported logout behavior %q", r.URL.Query().Get("behavior")))
39+
}
1740

1841
s := getSession(ctx)
1942
u := getUser(ctx)
@@ -22,15 +45,28 @@ func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
2245
if terr := models.NewAuditLogEntry(r, tx, u, models.LogoutAction, "", nil); terr != nil {
2346
return terr
2447
}
48+
2549
if s != nil {
26-
return models.Logout(tx, u.ID)
50+
return models.LogoutAllRefreshTokens(tx, u.ID)
51+
}
52+
53+
switch behavior {
54+
case LogoutLocal:
55+
return models.LogoutSession(tx, s.ID)
56+
57+
case LogoutOthers:
58+
return models.LogoutAllExceptMe(tx, s.ID, u.ID)
2759
}
28-
return models.LogoutAllRefreshTokens(tx, u.ID)
60+
61+
// default mode, log out everywhere
62+
return models.Logout(tx, u.ID)
2963
})
3064
if err != nil {
3165
return internalServerError("Error logging out user").WithInternalError(err)
3266
}
3367

68+
a.clearCookieTokens(config, w)
3469
w.WriteHeader(http.StatusNoContent)
70+
3571
return nil
3672
}

internal/api/logout_test.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"net/http"
66
"net/http/httptest"
7+
"net/url"
78
"testing"
89
"time"
910

@@ -48,18 +49,29 @@ func (ts *LogoutTestSuite) SetupTest() {
4849
}
4950

5051
func (ts *LogoutTestSuite) TestLogoutSuccess() {
51-
req := httptest.NewRequest(http.MethodPost, "http://localhost/logout", nil)
52-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
53-
w := httptest.NewRecorder()
52+
for _, behavior := range []string{"", "global", "local", "others"} {
53+
reqURL, err := url.ParseRequestURI("http://localhost/logout")
54+
require.NoError(ts.T(), err)
5455

55-
ts.API.handler.ServeHTTP(w, req)
56-
require.Equal(ts.T(), http.StatusNoContent, w.Code)
56+
if behavior != "" {
57+
query := reqURL.Query()
58+
query.Set("behavior", behavior)
59+
reqURL.RawQuery = query.Encode()
60+
}
61+
62+
req := httptest.NewRequest(http.MethodPost, reqURL.String(), nil)
63+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
64+
w := httptest.NewRecorder()
65+
66+
ts.API.handler.ServeHTTP(w, req)
67+
require.Equal(ts.T(), http.StatusNoContent, w.Code)
5768

58-
accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
59-
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
60-
for _, c := range w.Result().Cookies() {
61-
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
62-
require.Equal(ts.T(), "", c.Value)
69+
accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
70+
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
71+
for _, c := range w.Result().Cookies() {
72+
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
73+
require.Equal(ts.T(), "", c.Value)
74+
}
6375
}
6476
}
6577
}

openapi.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ paths:
155155
security:
156156
- APIKeyAuth: []
157157
UserAuth: []
158+
parameters:
159+
- name: behavior
160+
in: query
161+
description: >
162+
(Optional.) Determines how the user should be logged out. When `global` is used, the user is logged out from all active sessions. When `local` is used, the user is logged out from the current session. When `others` is used, the user is logged out from all other sessions except the current one. Clients should remove stored access and refresh tokens except when `others` is used.
163+
schema:
164+
type: string
165+
enum:
166+
- global
167+
- local
168+
- others
158169
responses:
159170
204:
160171
description: No content returned on successful logout.

0 commit comments

Comments
 (0)