Skip to content

Commit b73c06e

Browse files
authored
feat: configurable expiry for temporary proxy/tunnel API keys (#443)
The Ory Proxy and Tunnel create a temporary project API key to configure your project. These keys were created without expiry, so if the cleanup on shutdown failed (e.g. the process was killed) the key would linger indefinitely. Add an --api-key-expiry flag to both commands that sets a server-side expiry on the temporary key, ensuring it is removed automatically even when local cleanup does not run. Defaults to 12h; set to 0 to disable.
1 parent 52eb734 commit b73c06e

3 files changed

Lines changed: 42 additions & 9 deletions

File tree

cmd/cloudx/client/api_key.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,33 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"time"
1011

1112
cloud "github.com/ory/client-go"
1213
"github.com/ory/x/cmdx"
1314
)
1415

15-
func (h *CommandHelper) CreateProjectAPIKey(ctx context.Context, projectID, name string) (*cloud.ProjectApiKey, error) {
16+
// CreateProjectAPIKey creates a project API key. If expiresIn is greater than
17+
// zero, the key is set to expire that duration from now so it is cleaned up
18+
// automatically on the server side even if local cleanup fails. An expiresIn of
19+
// zero creates a key without expiry; a negative value is rejected.
20+
func (h *CommandHelper) CreateProjectAPIKey(ctx context.Context, projectID, name string, expiresIn time.Duration) (*cloud.ProjectApiKey, error) {
21+
if expiresIn < 0 {
22+
return nil, errors.New("API key expiry must not be negative")
23+
}
24+
1625
c, err := h.newConsoleAPIClient(ctx)
1726
if err != nil {
1827
return nil, err
1928
}
2029

21-
token, res, err := c.ProjectAPI.CreateProjectApiKey(ctx, projectID).CreateProjectApiKeyRequest(cloud.CreateProjectApiKeyRequest{Name: name}).Execute()
30+
req := cloud.CreateProjectApiKeyRequest{Name: name}
31+
if expiresIn > 0 {
32+
expiresAt := time.Now().Add(expiresIn)
33+
req.ExpiresAt = &expiresAt
34+
}
35+
36+
token, res, err := c.ProjectAPI.CreateProjectApiKey(ctx, projectID).CreateProjectApiKeyRequest(req).Execute()
2237
if err != nil {
2338
return nil, handleError("unable to create project API key", res, err)
2439
}
@@ -64,7 +79,11 @@ func (h *CommandHelper) DeleteWorkspaceAPIKey(ctx context.Context, workspaceID,
6479
return nil
6580
}
6681

67-
func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string) (apiKey string, cleanup func() error, err error) {
82+
// TemporaryAPIKey creates a short-lived project API key that is deleted via the
83+
// returned cleanup function. The key is additionally set to expire after
84+
// expiresIn so that it is removed automatically should the cleanup fail. An
85+
// expiresIn of zero creates a key without expiry.
86+
func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string, expiresIn time.Duration) (apiKey string, cleanup func() error, err error) {
6887
if h.projectAPIKey != nil {
6988
return *h.projectAPIKey, noop, nil
7089
}
@@ -95,7 +114,7 @@ func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string) (apiKe
95114
if err != nil {
96115
return "", noop, err
97116
}
98-
ak, err := h.CreateProjectAPIKey(ctx, projectID, name)
117+
ak, err := h.CreateProjectAPIKey(ctx, projectID, name, expiresIn)
99118
if err != nil {
100119
_, _ = fmt.Fprintf(h.VerboseErrWriter, "Unable to create API key. Do you have the required permissions to use the Ory CLI with project %q? Continuing without API key.", projectID)
101120
return "", noop, nil

cmd/cloudx/client/command_helper_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func TestCommandHelper(t *testing.T) {
155155
require.Len(t, projects, 2)
156156
assert.ElementsMatch(t, []string{p0.Id, p1.Id}, []string{projects[0].Id, projects[1].Id})
157157

158-
pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key")
158+
pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key", 0)
159159
require.NoError(t, err)
160160

161161
pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value))
@@ -192,7 +192,7 @@ func TestCommandHelper(t *testing.T) {
192192
require.Len(t, projects, 2)
193193
assert.ElementsMatch(t, []string{p0.Id, p1.Id}, []string{projects[0].Id, projects[1].Id})
194194

195-
pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key")
195+
pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key", 0)
196196
require.NoError(t, err)
197197

198198
pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value))
@@ -310,10 +310,13 @@ func TestCommandHelper(t *testing.T) {
310310

311311
keyName := "a test key"
312312

313-
key, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, keyName)
313+
key, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, keyName, time.Hour)
314314
require.NoError(t, err)
315315
assert.Equal(t, keyName, key.Name)
316316
assert.NotNil(t, keyName, key.Value)
317+
// The requested expiry should be reflected on the returned key.
318+
require.NotNil(t, key.ExpiresAt)
319+
assert.WithinDuration(t, time.Now().Add(time.Hour), *key.ExpiresAt, 5*time.Minute)
317320

318321
// check that the key works
319322
ctxWithKey := client.ContextWithOptions(ctx,
@@ -333,7 +336,7 @@ func TestCommandHelper(t *testing.T) {
333336
t.Run("func=GetProject", func(t *testing.T) {
334337
wsKeyH, err := client.NewCommandHelper(ctx, client.WithWorkspaceAPIKey(*defaultWorkspaceAPIKey.Value))
335338
require.NoError(t, err)
336-
pjKey, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, "test key")
339+
pjKey, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, "test key", 0)
337340
require.NoError(t, err)
338341
pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value))
339342
require.NoError(t, err)

cmd/cloudx/proxy/helpers.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ const (
5353
CORSFlag = "allowed-cors-origins"
5454
AdditionalCORSHeadersFlag = "additional-cors-headers"
5555
RewriteHostFlag = "rewrite-host"
56+
APIKeyExpiryFlag = "api-key-expiry"
5657
)
5758

59+
// defaultAPIKeyExpiry is the default lifetime of the temporary API key the
60+
// proxy and tunnel create to configure the project.
61+
const defaultAPIKeyExpiry = 12 * time.Hour
62+
5863
type config struct {
5964
port int
6065
open bool
@@ -70,6 +75,11 @@ type config struct {
7075
corsOrigins []string
7176
additionalCorsHeaders []string
7277

78+
// apiKeyExpiry is the lifetime of the temporary API key the proxy/tunnel
79+
// creates. The key is deleted on shutdown; the expiry ensures it is removed
80+
// automatically should that cleanup fail. A value of zero disables expiry.
81+
apiKeyExpiry time.Duration
82+
7383
// rewriteHost means the host header will be rewritten to the upstream host.
7484
// This is useful in cases where upstream resolves requests based on Host.
7585
rewriteHost bool
@@ -89,6 +99,7 @@ func registerConfigFlags(conf *config, flags *pflag.FlagSet) {
8999
flags.BoolVar(&conf.isDev, DevFlag, true, "This flag is deprecated as the command is only supposed to be used during development.")
90100
flags.BoolVar(&conf.isDebug, DebugFlag, false, "Use this flag to debug, for example, CORS requests.")
91101
flags.BoolVar(&conf.rewriteHost, RewriteHostFlag, false, "Use this flag to rewrite the host header to the upstream host.")
102+
flags.DurationVar(&conf.apiKeyExpiry, APIKeyExpiryFlag, defaultAPIKeyExpiry, "Sets the expiry of the temporary API key the Ory CLI creates to configure your project. The key is deleted on shutdown; this expiry ensures it is removed automatically if that cleanup fails. Set to 0 to disable expiry.")
92103
}
93104

94105
func portFromEnv() int {
@@ -115,7 +126,7 @@ func runReverseProxy(ctx context.Context, h *client.CommandHelper, stdErr io.Wri
115126
return err
116127
}
117128

118-
apiKey, removeAPIKey, err := h.TemporaryAPIKey(ctx, fmt.Sprintf("Ory %s temporary API key - %s", name, h.UserName(ctx)))
129+
apiKey, removeAPIKey, err := h.TemporaryAPIKey(ctx, fmt.Sprintf("Ory %s temporary API key - %s", name, h.UserName(ctx)), conf.apiKeyExpiry)
119130
if err != nil {
120131
return err
121132
}

0 commit comments

Comments
 (0)