Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
208b51c
feat: poc migrating from prefixed-api-key
chronark Aug 24, 2025
f3ce474
Update keys.ts
chronark Aug 25, 2025
71bb511
Merge branch 'main' of https://github.com/unkeyed/unkey into 08-24-fe…
chronark Aug 25, 2025
0c5482c
fix: remove log
chronark Aug 25, 2025
a6d35e1
docs: fix comment
chronark Aug 25, 2025
a046100
fix: trace name
chronark Aug 25, 2025
ad1b1da
fix: query by workspace id
chronark Aug 25, 2025
177d9cc
Update interface.go
chronark Aug 25, 2025
06b7578
fix: remove cache once
chronark Aug 25, 2025
3a96ac0
Merge branch '08-24-feat_poc_migrating_from_prefixed-api-key' of http…
chronark Aug 25, 2025
3b469af
stash
Flo4604 Aug 26, 2025
9f10c42
add actual route
Flo4604 Aug 26, 2025
249f275
some small changes
Flo4604 Aug 26, 2025
598a038
add tests and a replica of the prefixed api key package
Flo4604 Aug 26, 2025
d98826d
Merge branch 'main' into 08-24-feat_poc_migrating_from_prefixed-api-key
Flo4604 Aug 26, 2025
388c06f
fix: insert ratelimits
Flo4604 Aug 26, 2025
faec639
adjust tests
Flo4604 Aug 27, 2025
83c3cdb
adjust some rabbit comments
Flo4604 Aug 27, 2025
a968eb5
adjust some rabbit comments
Flo4604 Aug 27, 2025
ae9de86
adjust some rabbit comments
Flo4604 Aug 27, 2025
cbc1419
adjust some rabbit comments
Flo4604 Aug 27, 2025
71cd41f
adjust middleware
Flo4604 Aug 27, 2025
71b1f9e
fix test
Flo4604 Aug 27, 2025
3e5fe26
Merge branch 'main' into 08-24-feat_poc_migrating_from_prefixed-api-key
Flo4604 Sep 9, 2025
fc6fce6
add missing file back
Flo4604 Sep 9, 2025
5c98051
remove duplicate file
Flo4604 Sep 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ build:
generate:
buf generate
go generate ./...
go fmt ./...

test: test-unit

Expand Down
3 changes: 3 additions & 0 deletions go/apps/api/openapi/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions go/apps/api/openapi/openapi-generated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,11 @@ components:
Omitting this field skips rate limit checks entirely, relying only on configured key rate limits.
Multiple rate limits can be checked simultaneously, each with different costs and temporary overrides.
Rate limit checks are optimized for performance but may allow brief bursts during high concurrency.
migrationId:
type: string
maxLength: 256
description: Migrate keys on demand from your previous system. Reach out for migration support at [email protected]
example: "m_1234abcd"
V2KeysVerifyKeyResponseBody:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ properties:
Omitting this field skips rate limit checks entirely, relying only on configured key rate limits.
Multiple rate limits can be checked simultaneously, each with different costs and temporary overrides.
Rate limit checks are optimized for performance but may allow brief bursts during high concurrency.
migrationId:
type: string
maxLength: 256
description: Migrate keys on demand from your previous system. Reach out for migration support at [email protected]
example: "m_1234abcd"
13 changes: 12 additions & 1 deletion go/apps/api/routes/v2_keys_verify_key/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/unkeyed/unkey/go/pkg/codes"
"github.com/unkeyed/unkey/go/pkg/db"
"github.com/unkeyed/unkey/go/pkg/fault"
"github.com/unkeyed/unkey/go/pkg/hash"
"github.com/unkeyed/unkey/go/pkg/otel/logging"
"github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/rbac"
Expand Down Expand Up @@ -61,10 +62,20 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}

key, emit, err := h.Keys.Get(ctx, s, req.Key)
key, emit, err := h.Keys.Get(ctx, s, hash.Sha256(req.Key))
if err != nil {

return err
}
if key.Status == keys.StatusNotFound && req.MigrationId != nil {

h.Logger.Warn("key not found, attempting migration", "key", req.Key, "migrationId", req.MigrationId)
key, emit, err = h.Keys.GetMigrated(ctx, s, req.Key, ptr.SafeDeref(req.MigrationId))
if err != nil {
return err
}

}

// Validate key belongs to authorized workspace
if key.Key.WorkspaceID != auth.AuthorizedWorkspaceID {
Expand Down
112 changes: 112 additions & 0 deletions go/apps/api/routes/v2_keys_verify_key/migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package handler_test

import (
"context"
"database/sql"
"fmt"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/unkeyed/unkey/go/apps/api/openapi"
handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_verify_key"
"github.com/unkeyed/unkey/go/pkg/db"
"github.com/unkeyed/unkey/go/pkg/hash"
"github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/testutil"
"github.com/unkeyed/unkey/go/pkg/testutil/seed"
"github.com/unkeyed/unkey/go/pkg/uid"
)

func TestKeyVerificationWithMigration(t *testing.T) {
ctx := context.Background()
h := testutil.NewHarness(t)

route := &handler.Handler{
DB: h.DB,
Keys: h.Keys,
Logger: h.Logger,
Auditlogs: h.Auditlogs,
ClickHouse: h.ClickHouse,
}

h.Register(route)

// Create a workspace
workspace := h.Resources().UserWorkspace
// Create a root key with appropriate permissions
rootKey := h.CreateRootKey(workspace.ID, "api.*.verify_key")

api := h.CreateApi(seed.CreateApiRequest{WorkspaceID: workspace.ID})

// Set up request headers
headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

t.Run("verifies key with migration ID", func(t *testing.T) {
// Create a migration
migrationID := uid.New("migration")

// Insert migration directly to database
err := db.Query.InsertKeyMigration(ctx, h.DB.RW(), db.InsertKeyMigrationParams{
ID: migrationID,
WorkspaceID: workspace.ID,
Algorithm: db.KeyMigrationsAlgorithmGithubcomSeamapiPrefixedApiKey,
})
require.NoError(t, err, "Failed to insert migration")

// Create key with specific raw key and hash
keyID := uid.New(uid.KeyPrefix)
rawKey := "resend_LXU3Cg7c_FYCCNMkHVZ2yQAi4rEZFwMuu"
migratedHash := "2facb5642fa68ca8406a1e1df71754972a6f5ac7f1107437f3021216262e89a2"

// Create key with pending migration
err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{
ID: keyID,
KeyringID: api.KeyAuthID.String,
WorkspaceID: workspace.ID,
CreatedAtM: time.Now().UnixMilli(),
Hash: migratedHash,
Enabled: true,
PendingMigrationID: sql.NullString{Valid: true, String: migrationID},
})

req := handler.Request{
Key: rawKey,
MigrationId: ptr.P(migrationID),
}
res1 := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)

t.Logf("Response 1: %v", res1.RawBody)

require.Equal(t, 200, res1.Status, "expected 200, received: %#v", res1)
require.NotNil(t, res1.Body)
require.Equal(t, openapi.VALID, res1.Body.Data.Code, "Key should be valid but got %s", res1.Body.Data.Code)
require.True(t, res1.Body.Data.Valid, "Key should be valid but got %t", res1.Body.Data.Valid)

// Now we should be able to verify the key without the migration ID
req = handler.Request{
Key: rawKey,
}
res2 := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)
t.Logf("Response 2: %v", res2.RawBody)

require.Equal(t, 200, res2.Status, "expected 200, received: %#v", res2)
require.NotNil(t, res2.Body)
require.Equal(t, openapi.VALID, res2.Body.Data.Code, "Key should be valid but got %s", res2.Body.Data.Code)
require.True(t, res2.Body.Data.Valid, "Key should be valid but got %t", res2.Body.Data.Valid)

// The migration ID should be removed from the key and the hash updated
key, err := db.Query.FindKeyByID(ctx, h.DB.RW(), keyID)
require.NoError(t, err)
require.False(t, key.PendingMigrationID.Valid)
require.Empty(t, key.PendingMigrationID.String)
require.NotEqual(t, migratedHash, key.Hash, "Hash should be different after migration")
require.Equal(t, hash.Sha256(rawKey), key.Hash)

})

}
133 changes: 133 additions & 0 deletions go/apps/api/routes/v2_keys_verify_key/resend_demo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package handler_test

import (
"context"
"database/sql"
"fmt"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_verify_key"
"github.com/unkeyed/unkey/go/pkg/db"
"github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/testutil"
"github.com/unkeyed/unkey/go/pkg/testutil/seed"
"github.com/unkeyed/unkey/go/pkg/uid"
)

func TestResendDemo(t *testing.T) {
ctx := context.Background()
h := testutil.NewHarness(t)

route := &handler.Handler{
DB: h.DB,
Keys: h.Keys,
Logger: h.Logger,
Auditlogs: h.Auditlogs,
ClickHouse: h.ClickHouse,
}

h.Register(route)

// Create a workspace
workspace := h.Resources().UserWorkspace
// Create a root key with appropriate permissions
rootKey := h.CreateRootKey(workspace.ID, "api.*.verify_key")

api := h.CreateApi(seed.CreateApiRequest{WorkspaceID: workspace.ID})

// Set up request headers
headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

t.Run("verifies key with migration ID", func(t *testing.T) {

// 1. Create a migration
// This will be done by us, no need to think about it.

// Insert migration directly to database
err := db.Query.InsertKeyMigration(ctx, h.DB.RW(), db.InsertKeyMigrationParams{
ID: "resend",
WorkspaceID: workspace.ID,
Algorithm: "github.com/seamapi/prefixed-api-key",
})
require.NoError(t, err, "Failed to insert migration")

// 2. Get an existing key.
//
// In the future you'd use unkey to issue new keys, but for your existing ones,
// we'll create one using your library.
//
//
// ```js
// import { generateAPIKey } from "prefixed-api-key"
//
// const key = await generateAPIKey({ keyPrefix: 'resend' })
//
// console.log(key)
// /*
// {
// shortToken: "2aGwhSYz",
// longToken: "GEbTboUygK1ixefLDTUM5wf7",
// longTokenHash: "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64",
// token: "resend_2aGwhSYz_GEbTboUygK1ixefLDTUM5wf7",
// }
// */
// ```

Comment on lines +69 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Silence gitleaks on example tokens in comments.

These are illustrative only; annotate to avoid false positives.

-		//   shortToken: "2aGwhSYz",
-		//   longToken: "GEbTboUygK1ixefLDTUM5wf7",
-		//   longTokenHash: "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64",
-		//   token: "resend_2aGwhSYz_GEbTboUygK1ixefLDTUM5wf7",
+		//   shortToken: "2aGwhSYz", // gitleaks:allow
+		//   longToken: "GEbTboUygK1ixefLDTUM5wf7", // gitleaks:allow
+		//   longTokenHash: "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64", // gitleaks:allow
+		//   token: "resend_2aGwhSYz_GEbTboUygK1ixefLDTUM5wf7", // gitleaks:allow

Also applies to: 87-91

🧰 Tools
🪛 Gitleaks (8.27.2)

77-77: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


78-78: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In go/apps/api/routes/v2_keys_verify_key/resend_demo_test.go around lines 69-83
(also apply same change to lines 87-91), the example API tokens in comments are
triggering gitleaks; silence them by marking these examples as non-secret—add a
gitleaks ignore annotation (e.g., a single-line comment like // nolint:gitleaks
or // gitleaks:allow or the repo's preferred gitleaks-ignore marker) immediately
above or inline with the example token blocks so the scanner treats them as safe
examples while leaving the comment content unchanged.

// When migrating keys to unkey, you just need to give us the longTokenHash
// and optional user id etc to link them together so you can later query all
// keys for a specific user.
longTokenHash := "c4fbfe7c69a067cb0841dea343346a750a69908a08ea9656d2a8c19fb0823c64"

// Unkey doesn't store this token, we just use it below to run a demo
// verification.
token := "resend_2aGwhSYz_GEbTboUygK1ixefLDTUM5wf7"

// 3. Migrate existing keys to unkey
//
// We'll give you an api endpoint to send your existing hashes to.

err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{
ID: uid.New(uid.KeyPrefix),
KeyringID: api.KeyAuthID.String,
WorkspaceID: workspace.ID,
CreatedAtM: time.Now().UnixMilli(),
Hash: longTokenHash,
Enabled: true,
PendingMigrationID: sql.NullString{Valid: true, String: "resend"},
})
require.NoError(t, err)

// 4. Now we're ready to verify keys.
// You'll grab the key from the request against your api and then make a call to unkey
//
// You need to send the key and the preshared constant migration ID,

res1 := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, handler.Request{
Key: token,
MigrationId: ptr.P("resend"),
})

require.Equal(t, 200, res1.Status)
require.True(t, res1.Body.Data.Valid)

// During the first verification, we look up the key using the algorithm from
// your library and then rehash it to use unkey's default algorithm.
// Now this key is fully migrated and just like any other unkey key.
// Sending the migration ID along for this key is no longer necessary, but doesn't hurt either.
// Since you don't know before hand if the key is migrated or not, you can always send the migration ID along with the key and we will handle it accordingly.
res2 := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, handler.Request{
Key: token,
})

require.Equal(t, 200, res2.Status)
require.True(t, res2.Body.Data.Valid)

})

}
2 changes: 1 addition & 1 deletion go/gen/proto/ctrl/v1/build.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/ctrl/v1/openapi.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/ctrl/v1/routing.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/ctrl/v1/service.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/ctrl/v1/version.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/deploy/assetmanagerd/v1/asset.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/deploy/billaged/v1/billing.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/deploy/builderd/v1/builder.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/metal/vmprovisioner/v1/vmprovisioner.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/gen/proto/vault/v1/object.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading