Skip to content

Commit 8bba754

Browse files
committed
feat: add different logout behaviors
1 parent 773e45e commit 8bba754

File tree

3 files changed

+76
-13
lines changed

3 files changed

+76
-13
lines changed

internal/api/logout.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
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+
if r.URL.Query() != nil {
28+
switch r.URL.Query().Get("behavior") {
29+
case "", "global":
30+
behavior = LogoutGlobal
31+
32+
case "local":
33+
behavior = LogoutLocal
34+
35+
case "others":
36+
behavior = LogoutOthers
37+
38+
default:
39+
return badRequestError(fmt.Sprintf("Unsupported logout behavior %q", r.URL.Query().Get("behavior")))
40+
}
41+
}
1742

1843
s := getSession(ctx)
1944
u := getUser(ctx)
@@ -22,15 +47,28 @@ func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
2247
if terr := models.NewAuditLogEntry(r, tx, u, models.LogoutAction, "", nil); terr != nil {
2348
return terr
2449
}
50+
2551
if s != nil {
26-
return models.Logout(tx, u.ID)
52+
return models.LogoutAllRefreshTokens(tx, u.ID)
53+
}
54+
55+
switch behavior {
56+
case LogoutLocal:
57+
return models.LogoutSession(tx, s.ID)
58+
59+
case LogoutOthers:
60+
return models.LogoutAllExceptMe(tx, s.ID, u.ID)
2761
}
28-
return models.LogoutAllRefreshTokens(tx, u.ID)
62+
63+
// default mode, log out everywhere
64+
return models.Logout(tx, u.ID)
2965
})
3066
if err != nil {
3167
return internalServerError("Error logging out user").WithInternalError(err)
3268
}
3369

70+
a.clearCookieTokens(config, w)
3471
w.WriteHeader(http.StatusNoContent)
72+
3573
return nil
3674
}

internal/api/logout_test.go

Lines changed: 24 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

910
"github.com/stretchr/testify/require"
@@ -47,18 +48,31 @@ func (ts *LogoutTestSuite) SetupTest() {
4748
}
4849

4950
func (ts *LogoutTestSuite) TestLogoutSuccess() {
50-
req := httptest.NewRequest(http.MethodPost, "http://localhost/logout", nil)
51-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
52-
w := httptest.NewRecorder()
51+
for _, behavior := range []string{"", "global", "local", "others"} {
52+
ts.SetupTest()
5353

54-
ts.API.handler.ServeHTTP(w, req)
55-
require.Equal(ts.T(), http.StatusNoContent, w.Code)
54+
reqURL, err := url.ParseRequestURI("http://localhost/logout")
55+
require.NoError(ts.T(), err)
5656

57-
accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
58-
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
59-
for _, c := range w.Result().Cookies() {
60-
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
61-
require.Equal(ts.T(), "", c.Value)
57+
if behavior != "" {
58+
query := reqURL.Query()
59+
query.Set("behavior", behavior)
60+
reqURL.RawQuery = query.Encode()
61+
}
62+
63+
req := httptest.NewRequest(http.MethodPost, reqURL.String(), nil)
64+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
65+
w := httptest.NewRecorder()
66+
67+
ts.API.handler.ServeHTTP(w, req)
68+
require.Equal(ts.T(), http.StatusNoContent, w.Code)
69+
70+
accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
71+
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
72+
for _, c := range w.Result().Cookies() {
73+
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
74+
require.Equal(ts.T(), "", c.Value)
75+
}
6276
}
6377
}
6478
}

openapi.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ paths:
158158
security:
159159
- APIKeyAuth: []
160160
UserAuth: []
161+
parameters:
162+
- name: behavior
163+
in: query
164+
description: >
165+
(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.
166+
schema:
167+
type: string
168+
enum:
169+
- global
170+
- local
171+
- others
161172
responses:
162173
204:
163174
description: No content returned on successful logout.

0 commit comments

Comments
 (0)