-
Notifications
You must be signed in to change notification settings - Fork 584
feat: poc migrating from prefixed-api-key #3841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
208b51c
f3ce474
71bb511
0c5482c
a6d35e1
a046100
ad1b1da
177d9cc
06b7578
3a96ac0
3b469af
9f10c42
249f275
598a038
d98826d
388c06f
faec639
83c3cdb
a968eb5
ae9de86
cbc1419
71cd41f
71b1f9e
3e5fe26
fc6fce6
5c98051
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ build: | |
generate: | ||
buf generate | ||
go generate ./... | ||
go fmt ./... | ||
|
||
test: test-unit | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" |
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) | ||
|
||
Flo4604 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}) | ||
|
||
} |
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") | ||
Flo4604 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
// 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) | ||
|
||
}) | ||
|
||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.