Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
c2aea5a4
Commit
c2aea5a4
authored
Dec 30, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement refresh token rotation with sliding window sessions in the auth service
parent
d55af9b5
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
43 additions
and
7 deletions
+43
-7
auth_service.go
server/router/api/v1/auth_service.go
+43
-7
No files found.
server/router/api/v1/auth_service.go
View file @
c2aea5a4
...
...
@@ -271,14 +271,17 @@ func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*em
// RefreshToken exchanges a valid refresh token for a new access token.
//
// This endpoint:
// This endpoint
implements refresh token rotation with sliding window sessions
:
// 1. Extracts the refresh token from the HttpOnly cookie (memos_refresh)
// 2. Validates the refresh token against the database (checking expiry and revocation)
// 3. Generates a new short-lived access token (15 minutes)
// 4. Returns the new access token and its expiry time
// 3. Rotates the refresh token: generates a new one with fresh 30-day expiry
// 4. Generates a new short-lived access token (15 minutes)
// 5. Sets the new refresh token as HttpOnly cookie
// 6. Returns the new access token and its expiry time
//
// The refresh token remains valid and is not rotated.
// Client should store the new access token in memory and use it for API requests.
// Token rotation provides:
// - Sliding window sessions: active users stay logged in indefinitely
// - Better security: stolen refresh tokens become invalid after legitimate refresh
//
// Authentication: Requires valid refresh token in cookie (public endpoint)
// Returns: New access token and expiry timestamp.
...
...
@@ -295,13 +298,46 @@ func (s *APIV1Service) RefreshToken(ctx context.Context, _ *v1pb.RefreshTokenReq
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"refresh token not found"
)
}
// Validate refresh token
// Validate refresh token
and get old token ID for rotation
authenticator
:=
auth
.
NewAuthenticator
(
s
.
Store
,
s
.
Secret
)
user
,
_
,
err
:=
authenticator
.
AuthenticateByRefreshToken
(
ctx
,
refreshToken
)
user
,
oldTokenID
,
err
:=
authenticator
.
AuthenticateByRefreshToken
(
ctx
,
refreshToken
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"invalid refresh token: %v"
,
err
)
}
// --- Refresh Token Rotation ---
// Generate new refresh token with fresh 30-day expiry (sliding window)
newTokenID
:=
util
.
GenUUID
()
newRefreshToken
,
newRefreshExpiresAt
,
err
:=
auth
.
GenerateRefreshToken
(
user
.
ID
,
newTokenID
,
[]
byte
(
s
.
Secret
))
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to generate refresh token: %v"
,
err
)
}
// Store new refresh token (add before remove to handle race conditions)
clientInfo
:=
s
.
extractClientInfo
(
ctx
)
newRefreshTokenRecord
:=
&
storepb
.
RefreshTokensUserSetting_RefreshToken
{
TokenId
:
newTokenID
,
ExpiresAt
:
timestamppb
.
New
(
newRefreshExpiresAt
),
CreatedAt
:
timestamppb
.
Now
(),
ClientInfo
:
clientInfo
,
}
if
err
:=
s
.
Store
.
AddUserRefreshToken
(
ctx
,
user
.
ID
,
newRefreshTokenRecord
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to store refresh token: %v"
,
err
)
}
// Remove old refresh token
if
err
:=
s
.
Store
.
RemoveUserRefreshToken
(
ctx
,
user
.
ID
,
oldTokenID
);
err
!=
nil
{
// Log but don't fail - old token will expire naturally
slog
.
Warn
(
"failed to remove old refresh token"
,
"error"
,
err
,
"userID"
,
user
.
ID
,
"tokenID"
,
oldTokenID
)
}
// Set new refresh token cookie
newRefreshCookie
:=
s
.
buildRefreshTokenCookie
(
ctx
,
newRefreshToken
,
newRefreshExpiresAt
)
if
err
:=
SetResponseHeader
(
ctx
,
"Set-Cookie"
,
newRefreshCookie
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to set refresh token cookie: %v"
,
err
)
}
// --- End Rotation ---
// Generate new access token
accessToken
,
expiresAt
,
err
:=
auth
.
GenerateAccessTokenV2
(
user
.
ID
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment