feat(auth): add ability to regenerate access tokens (#6963)

- Add the ability to regenerate existing access tokens in the UI. This preserves the ID of the access token, but generates a new salt and token contents.
- Integration test added.
- Unit test added.
- Resolves #6880

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6963
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
Co-committed-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
This commit is contained in:
Dmitrii Sharshakov 2025-03-08 10:42:36 +00:00 committed by Gusted
parent 9dea54a9d6
commit 30982b9e7b
8 changed files with 176 additions and 7 deletions

View file

@ -98,6 +98,15 @@ func init() {
// NewAccessToken creates new access token.
func NewAccessToken(ctx context.Context, t *AccessToken) error {
err := generateAccessToken(t)
if err != nil {
return err
}
_, err = db.GetEngine(ctx).Insert(t)
return err
}
func generateAccessToken(t *AccessToken) error {
salt, err := util.CryptoRandomString(10)
if err != nil {
return err
@ -110,8 +119,7 @@ func NewAccessToken(ctx context.Context, t *AccessToken) error {
t.Token = hex.EncodeToString(token)
t.TokenHash = HashToken(t.Token, t.TokenSalt)
t.TokenLastEight = t.Token[len(t.Token)-8:]
_, err = db.GetEngine(ctx).Insert(t)
return err
return nil
}
// DisplayPublicOnly whether to display this as a public-only token.
@ -234,3 +242,25 @@ func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
}
return nil
}
// RegenerateAccessTokenByID regenerates access token by given ID.
// It regenerates token and salt, as well as updates the creation time.
func RegenerateAccessTokenByID(ctx context.Context, id, userID int64) (*AccessToken, error) {
t := &AccessToken{}
found, err := db.GetEngine(ctx).Where("id = ? AND uid = ?", id, userID).Get(t)
if err != nil {
return nil, err
} else if !found {
return nil, ErrAccessTokenNotExist{}
}
err = generateAccessToken(t)
if err != nil {
return nil, err
}
// Reset the creation time, token is unused
t.UpdatedUnix = timeutil.TimeStampNow()
return t, UpdateAccessToken(ctx, t)
}

View file

@ -131,3 +131,28 @@ func TestDeleteAccessTokenByID(t *testing.T) {
require.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
}
func TestRegenerateAccessTokenByID(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "4c6f36e6cf498e2a448662f915d932c09c5a146c")
require.NoError(t, err)
newToken, err := auth_model.RegenerateAccessTokenByID(db.DefaultContext, token.ID, 1)
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: token.ID, UID: token.UID, TokenHash: token.TokenHash})
newToken = &auth_model.AccessToken{
ID: newToken.ID,
UID: newToken.UID,
TokenHash: newToken.TokenHash,
}
unittest.AssertExistsAndLoadBean(t, newToken)
// Token has been recreated, new salt and hash, but should retain the same ID, UID, Name and Scope
assert.Equal(t, token.ID, newToken.ID)
assert.NotEqual(t, token.TokenHash, newToken.TokenHash)
assert.NotEqual(t, token.TokenSalt, newToken.TokenSalt)
assert.Equal(t, token.UID, newToken.UID)
assert.Equal(t, token.Name, newToken.Name)
assert.Equal(t, token.Scope, newToken.Scope)
}