mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 16:01:16 +00:00
218af13dd9
Add authenticated session listing and revocation APIs, track the auth provider on sessions, and expose active session controls in account security settings.
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
package token
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/pkg/crypt"
|
|
)
|
|
|
|
func (s *tokenService) RefreshToken(ctx context.Context, refreshToken string) (*models.Token, *errx.Error) {
|
|
t, err := s.VerifyToken(refreshToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if t.ExpiresAt.Before(time.Now()) {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
sess, err := s.GetSession(ctx, t.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Don't let a revoked session refresh itself back to life.
|
|
if sess.RevokedAt != nil {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
if sess.RefreshNonce != t.Nonce {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
issuedAt := time.Now()
|
|
|
|
accessTokenExpiresAt := issuedAt.Add(AccessTokenLifeTime)
|
|
accessNonce, xerr := crypt.Nonce()
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return nil, errx.InternalError()
|
|
}
|
|
|
|
newAccessToken, xerr := s.GenerateToken(sess.UserID, sess.ID, "", accessNonce, issuedAt, accessTokenExpiresAt)
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return nil, errx.InternalError()
|
|
}
|
|
|
|
refreshTokenExpiresAt := issuedAt.Add(RefreshTokenLifeTime)
|
|
refreshNonce, xerr := crypt.Nonce()
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return nil, errx.InternalError()
|
|
}
|
|
|
|
newRefreshToken, xerr := s.GenerateToken(sess.UserID, sess.ID, "", refreshNonce, issuedAt, refreshTokenExpiresAt)
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return nil, errx.InternalError()
|
|
}
|
|
|
|
if err := s.tokenRepository.RefreshToken(ctx, sess.ID, t.Nonce, accessNonce, refreshNonce, issuedAt); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.InternalError()
|
|
}
|
|
|
|
// Invalidate the cached session. Postgres now has the new nonces, but
|
|
// Redis still has the old ones from when the session was last read. The
|
|
// very next API call would arrive with the NEW access nonce, GetSession
|
|
// would return the stale cached row with the OLD access nonce, the
|
|
// mismatch check would 401, and the frontend would log the user out.
|
|
// Dropping the cache forces the next GetSession to re-read from the
|
|
// updated Postgres row.
|
|
if err := s.deleteSession(ctx, sess.ID); err != nil {
|
|
sentry.CaptureException(err)
|
|
// Don't fail the refresh — the worst case if the delete somehow
|
|
// failed is the user retries; we already returned the new tokens.
|
|
}
|
|
|
|
return &models.Token{
|
|
AccessToken: newAccessToken,
|
|
AccessTokenExpiresAt: accessTokenExpiresAt,
|
|
RefreshToken: newRefreshToken,
|
|
RefreshTokenExpiresAt: refreshTokenExpiresAt,
|
|
}, nil
|
|
}
|