Skip to content

Commit

Permalink
feat: add different logout behaviors
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed May 23, 2023
1 parent 23c8b45 commit 32e5311
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 13 deletions.
44 changes: 41 additions & 3 deletions internal/api/logout.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
package api

import (
"fmt"
"net/http"

"github.com/supabase/gotrue/internal/models"
"github.com/supabase/gotrue/internal/storage"
)

type LogoutBehavior string

const (
LogoutGlobal LogoutBehavior = "global"
LogoutLocal LogoutBehavior = "local"
LogoutOthers LogoutBehavior = "others"
)

// Logout is the endpoint for logging out a user and thereby revoking any refresh tokens
func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config

a.clearCookieTokens(config, w)
behavior := LogoutGlobal

if r.URL.Query() != nil {
switch r.URL.Query().Get("behavior") {
case "", "global":
behavior = LogoutGlobal

case "local":
behavior = LogoutLocal

case "others":
behavior = LogoutOthers

default:
return badRequestError(fmt.Sprintf("Unsupported logout behavior %q", r.URL.Query().Get("behavior")))
}
}

s := getSession(ctx)
u := getUser(ctx)
Expand All @@ -22,15 +47,28 @@ func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
if terr := models.NewAuditLogEntry(r, tx, u, models.LogoutAction, "", nil); terr != nil {
return terr
}

if s != nil {
return models.Logout(tx, u.ID)
return models.LogoutAllRefreshTokens(tx, u.ID)
}

switch behavior {
case LogoutLocal:
return models.LogoutSession(tx, s.ID)

case LogoutOthers:
return models.LogoutAllExceptMe(tx, s.ID, u.ID)
}
return models.LogoutAllRefreshTokens(tx, u.ID)

// default mode, log out everywhere
return models.Logout(tx, u.ID)
})
if err != nil {
return internalServerError("Error logging out user").WithInternalError(err)
}

a.clearCookieTokens(config, w)
w.WriteHeader(http.StatusNoContent)

return nil
}
34 changes: 24 additions & 10 deletions internal/api/logout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

Expand Down Expand Up @@ -48,18 +49,31 @@ func (ts *LogoutTestSuite) SetupTest() {
}

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

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

accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
for _, c := range w.Result().Cookies() {
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
require.Equal(ts.T(), "", c.Value)
if behavior != "" {
query := reqURL.Query()
query.Set("behavior", behavior)
reqURL.RawQuery = query.Encode()
}

req := httptest.NewRequest(http.MethodPost, reqURL.String(), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
w := httptest.NewRecorder()

ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusNoContent, w.Code)

accessTokenKey := fmt.Sprintf("%v-access-token", ts.Config.Cookie.Key)
refreshTokenKey := fmt.Sprintf("%v-refresh-token", ts.Config.Cookie.Key)
for _, c := range w.Result().Cookies() {
if c.Name == accessTokenKey || c.Name == refreshTokenKey {
require.Equal(ts.T(), "", c.Value)
}
}
}
}
11 changes: 11 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ paths:
security:
- APIKeyAuth: []
UserAuth: []
parameters:
- name: behavior
in: query
description: >
(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.
schema:
type: string
enum:
- global
- local
- others
responses:
204:
description: No content returned on successful logout.
Expand Down

0 comments on commit 32e5311

Please sign in to comment.