mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): ensure that clearing invalid oauth refresh tokens works with dbcrypt (#15721)
https://github.com/coder/coder/pull/15608 introduced a buggy behaviour with dbcrypt enabled. When clearing an oauth refresh token, we had been setting the value to the empty string. The database encryption package considers decrypting an empty string to be an error, as an empty encrypted string value will still have a nonce associated with it and thus not actually be empty when stored at rest. Instead of 'deleting' the refresh token, 'update' it to be the empty string. This plays nicely with dbcrypt. It also adds a 'utility test' in the dbcrypt package to help encrypt a value. This was useful when manually fixing users affected by this bug on our dogfood instance.
This commit is contained in:
@@ -3,6 +3,8 @@ package dbcrypt
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -89,3 +91,35 @@ func TestCiphersBackwardCompatibility(t *testing.T) {
|
||||
require.NoError(t, err, "decryption should succeed")
|
||||
require.Equal(t, msg, string(decrypted), "decrypted message should match original message")
|
||||
}
|
||||
|
||||
// If you're looking here, you're probably in trouble.
|
||||
// Here's what you need to do:
|
||||
// 1. Get the current CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS environment variable.
|
||||
// 2. Run the following command:
|
||||
// ENCRYPT_ME="<value to encrypt>" CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS="<secret keys here>" go test -v -count=1 ./enterprise/dbcrypt -test.run='^TestHelpMeEncryptSomeValue$'
|
||||
// 3. Copy the value from the test output and do what you need with it.
|
||||
func TestHelpMeEncryptSomeValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Skip("this only exists if you need to encrypt a value with dbcrypt, it does not actually test anything")
|
||||
|
||||
valueToEncrypt := os.Getenv("ENCRYPT_ME")
|
||||
t.Logf("valueToEncrypt: %q", valueToEncrypt)
|
||||
keys := os.Getenv("CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS")
|
||||
require.NotEmpty(t, keys, "Set the CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS environment variable to use this")
|
||||
|
||||
base64Keys := strings.Split(keys, ",")
|
||||
activeKey := base64Keys[0]
|
||||
|
||||
decodedKey, err := base64.StdEncoding.DecodeString(activeKey)
|
||||
require.NoError(t, err, "the active key should be valid base64")
|
||||
|
||||
cipher, err := cipherAES256(decodedKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("cipher digest: %+v", cipher.HexDigest())
|
||||
|
||||
encryptedEmptyString, err := cipher.Encrypt([]byte(valueToEncrypt))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("encrypted and base64-encoded: %q", base64.StdEncoding.EncodeToString(encryptedEmptyString))
|
||||
}
|
||||
|
||||
@@ -261,6 +261,21 @@ func (db *dbCrypt) UpdateExternalAuthLink(ctx context.Context, params database.U
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (db *dbCrypt) UpdateExternalAuthLinkRefreshToken(ctx context.Context, params database.UpdateExternalAuthLinkRefreshTokenParams) error {
|
||||
// We would normally use a sql.NullString here, but sqlc does not want to make
|
||||
// a params struct with a nullable string.
|
||||
var digest sql.NullString
|
||||
if params.OAuthRefreshTokenKeyID != "" {
|
||||
digest.String = params.OAuthRefreshTokenKeyID
|
||||
digest.Valid = true
|
||||
}
|
||||
if err := db.encryptField(¶ms.OAuthRefreshToken, &digest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Store.UpdateExternalAuthLinkRefreshToken(ctx, params)
|
||||
}
|
||||
|
||||
func (db *dbCrypt) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
|
||||
keys, err := db.Store.GetCryptoKeys(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
)
|
||||
|
||||
func TestUserLinks(t *testing.T) {
|
||||
@@ -96,6 +97,31 @@ func TestUserLinks(t *testing.T) {
|
||||
require.EqualValues(t, expectedClaims, rawLink.Claims)
|
||||
})
|
||||
|
||||
t.Run("UpdateExternalAuthLinkRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, crypt, ciphers := setup(t)
|
||||
user := dbgen.User(t, crypt, database.User{})
|
||||
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
|
||||
UserID: user.ID,
|
||||
})
|
||||
|
||||
err := crypt.UpdateExternalAuthLinkRefreshToken(ctx, database.UpdateExternalAuthLinkRefreshTokenParams{
|
||||
OAuthRefreshToken: "",
|
||||
OAuthRefreshTokenKeyID: link.OAuthRefreshTokenKeyID.String,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ProviderID: link.ProviderID,
|
||||
UserID: link.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rawLink, err := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
||||
ProviderID: link.ProviderID,
|
||||
UserID: link.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "")
|
||||
})
|
||||
|
||||
t.Run("GetUserLinkByLinkedID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user