mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
13ca9ead3a
This PR uses the same sha256 hashing technique as we use for APIKeys. So now all randomly generated secrets will be hashed with sha256 for consistency. This is a breaking change for the oauth tokens. Since oauth is only allowed for dev builds and experimental, this is ok.
189 lines
5.5 KiB
Go
189 lines
5.5 KiB
Go
package apikey_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/apikey"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
)
|
|
|
|
func TestGenerate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type testcase struct {
|
|
name string
|
|
params apikey.CreateParams
|
|
fail bool
|
|
}
|
|
|
|
cases := []testcase{
|
|
{
|
|
name: "OK",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Duration(0),
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
LifetimeSeconds: int64(time.Hour.Seconds()),
|
|
TokenName: "hello",
|
|
RemoteAddr: "1.2.3.4",
|
|
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidScope",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Duration(0),
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
LifetimeSeconds: int64(time.Hour.Seconds()),
|
|
TokenName: "hello",
|
|
RemoteAddr: "1.2.3.4",
|
|
Scope: database.APIKeyScope("test"),
|
|
},
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "DeploymentSessionDuration",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Hour,
|
|
LifetimeSeconds: 0,
|
|
ExpiresAt: time.Time{},
|
|
TokenName: "hello",
|
|
RemoteAddr: "1.2.3.4",
|
|
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
|
},
|
|
},
|
|
{
|
|
name: "LifetimeSeconds",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Duration(0),
|
|
LifetimeSeconds: int64(time.Hour.Seconds()),
|
|
ExpiresAt: time.Time{},
|
|
TokenName: "hello",
|
|
RemoteAddr: "1.2.3.4",
|
|
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
|
},
|
|
},
|
|
{
|
|
name: "DefaultIP",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Duration(0),
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
LifetimeSeconds: int64(time.Hour.Seconds()),
|
|
TokenName: "hello",
|
|
RemoteAddr: "",
|
|
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
|
},
|
|
},
|
|
{
|
|
name: "DefaultScope",
|
|
params: apikey.CreateParams{
|
|
UserID: uuid.New(),
|
|
LoginType: database.LoginTypeOIDC,
|
|
DefaultLifetime: time.Duration(0),
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
LifetimeSeconds: int64(time.Hour.Seconds()),
|
|
TokenName: "hello",
|
|
RemoteAddr: "1.2.3.4",
|
|
Scope: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, keystr, err := apikey.Generate(tc.params)
|
|
if tc.fail {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, keystr)
|
|
require.NotEmpty(t, key.ID)
|
|
require.NotEmpty(t, key.HashedSecret)
|
|
|
|
// Assert the string secret is formatted correctly
|
|
keytokens := strings.Split(keystr, "-")
|
|
require.Len(t, keytokens, 2)
|
|
require.Equal(t, key.ID, keytokens[0])
|
|
|
|
// Assert that the hashed secret is correct.
|
|
equal := apikey.ValidateHash(key.HashedSecret, keytokens[1])
|
|
require.True(t, equal, "valid secret")
|
|
|
|
assert.Equal(t, tc.params.UserID, key.UserID)
|
|
assert.WithinDuration(t, dbtime.Now(), key.CreatedAt, time.Second*5)
|
|
assert.WithinDuration(t, dbtime.Now(), key.UpdatedAt, time.Second*5)
|
|
|
|
switch {
|
|
case tc.params.LifetimeSeconds > 0:
|
|
assert.Equal(t, tc.params.LifetimeSeconds, key.LifetimeSeconds)
|
|
case !tc.params.ExpiresAt.IsZero():
|
|
// Should not be a delta greater than 5 seconds.
|
|
assert.InDelta(t, time.Until(tc.params.ExpiresAt).Seconds(), key.LifetimeSeconds, 5)
|
|
default:
|
|
assert.Equal(t, int64(tc.params.DefaultLifetime.Seconds()), key.LifetimeSeconds)
|
|
}
|
|
|
|
switch {
|
|
case !tc.params.ExpiresAt.IsZero():
|
|
assert.Equal(t, tc.params.ExpiresAt.UTC(), key.ExpiresAt)
|
|
case tc.params.LifetimeSeconds > 0:
|
|
assert.WithinDuration(t, dbtime.Now().Add(time.Duration(tc.params.LifetimeSeconds)*time.Second), key.ExpiresAt, time.Second*5)
|
|
default:
|
|
assert.WithinDuration(t, dbtime.Now().Add(tc.params.DefaultLifetime), key.ExpiresAt, time.Second*5)
|
|
}
|
|
|
|
if tc.params.RemoteAddr != "" {
|
|
assert.Equal(t, tc.params.RemoteAddr, key.IPAddress.IPNet.IP.String())
|
|
} else {
|
|
assert.Equal(t, "0.0.0.0", key.IPAddress.IPNet.IP.String())
|
|
}
|
|
|
|
if tc.params.Scope != "" {
|
|
assert.True(t, key.Scopes.Has(tc.params.Scope))
|
|
} else {
|
|
assert.True(t, key.Scopes.Has(database.ApiKeyScopeCoderAll))
|
|
}
|
|
|
|
if tc.params.TokenName != "" {
|
|
assert.Equal(t, tc.params.TokenName, key.TokenName)
|
|
}
|
|
if tc.params.LoginType != "" {
|
|
assert.Equal(t, tc.params.LoginType, key.LoginType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInvalid just ensures the false case is asserted by some tests.
|
|
// Otherwise, a function that just `returns true` might pass all tests incorrectly.
|
|
func TestInvalid(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Falsef(t, apikey.ValidateHash([]byte{}, "secret"), "empty hash")
|
|
|
|
secret, hash, err := apikey.GenerateSecret(10)
|
|
require.NoError(t, err)
|
|
|
|
require.Falsef(t, apikey.ValidateHash(hash, secret+"_"), "different secret")
|
|
require.Falsef(t, apikey.ValidateHash(hash[:len(hash)-1], secret), "different hash length")
|
|
}
|