8000 feat: variable token expiration by grutt · Pull Request #670 · hatchet-dev/hatchet · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: variable token expiration #670

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

Merged
merged 4 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions api-contracts/openapi/components/schemas/api_tokens.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ CreateAPITokenRequest:
type: string
description: A name for the API token.
maxLength: 255
expiresIn:
type: string
description: The duration for which the token is valid.
x-oapi-codegen-extra-tags:
validate: "omitnil,duration"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may be losing my mind, but this validator seems to not accept the things it says it accepts. It will not accept y or M for example.

	DurationErr    = "Invalid duration. Durations must be in the format <number><unit>, where unit is one of: 's', 'm', 'h', 'd', 'w', 'M', 'y'"

I thought we already addressed this but I can't find the PR.

required:
- name

Expand Down
19 changes: 18 additions & 1 deletion api/v1/server/handlers/api-tokens/create.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package apitokens

import (
"time"

"github.com/labstack/echo/v4"

"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/db"
)
Expand All @@ -17,7 +20,21 @@ func (a *APITokenService) ApiTokenCreate(ctx echo.Context, request gen.ApiTokenC
return gen.ApiTokenCreate400JSONResponse(*apiErrors), nil
}

token, err := a.config.Auth.JWTManager.GenerateTenantToken(ctx.Request().Context(), tenant.ID, request.Body.Name)
var expiresAt *time.Time

if request.Body.ExpiresIn != nil {
expiresIn, err := time.ParseDuration(*request.Body.ExpiresIn)

if err != nil {
return gen.ApiTokenCreate400JSONResponse(apierrors.NewAPIErrors("invalid expiration duration")), nil
}

e := time.Now().UTC().Add(expiresIn)

expiresAt = &e
}

token, err := a.config.Auth.JWTManager.GenerateTenantToken(ctx.Request().Context(), tenant.ID, request.Body.Name, expiresAt)

if err != nil {
return nil, err
Expand Down
273 changes: 138 additions & 135 deletions api/v1/server/oas/gen/openapi.gen.go

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions cmd/hatchet-admin/cli/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"time"

"github.com/spf13/cobra"

Expand All @@ -15,6 +16,7 @@ import (
var (
tokenTenantId string
tokenName string
expiresIn time.Duration
)

var tokenCmd = &cobra.Command{
Expand All @@ -26,7 +28,7 @@ var tokenCreateAPICmd = &cobra.Command{
Use: "create",
Short: "create a new API token.",
Run: func(cmd *cobra.Command, args []string) {
err := runCreateAPIToken()
err := runCreateAPIToken(expiresIn)

if err != nil {
log.Printf("Fatal: could not run [token create] command: %v", err)
Expand Down Expand Up @@ -55,9 +57,18 @@ func init() {
"default",
"the name of the token",
)

tokenCreateAPICmd.PersistentFlags().DurationVarP(
&expiresIn,
"expiresIn",
"e",
90*24*time.Hour,
"Expiration duration for the API token",
)

}

func runCreateAPIToken() error {
func runCreateAPIToken(expiresIn time.Duration) error {
// read in the local config
configLoader := loader.NewConfigLoader(configDirectory)

Expand All @@ -77,7 +88,9 @@ func runCreateAPIToken() error {

defer serverConf.Disconnect() // nolint:errcheck

defaultTok, err := serverConf.Auth.JWTManager.GenerateTenantToken(context.Background(), tokenTenantId, tokenName)
expiresAt := time.Now().UTC().Add(expiresIn)

defaultTok, err := serverConf.Auth.JWTManager.GenerateTenantToken(context.Background(), tokenTenantId, tokenName, &expiresAt)

if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/src/lib/api/generated/data-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@ export interface CreateAPITokenRequest {
* @maxLength 255
*/
name: string;
/** The duration for which the token is valid. */
expiresIn?: string;
}

export interface CreateAPITokenResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,30 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { Spinner } from '@/components/ui/loading';
import { CodeHighlighter } from '@/components/ui/code-highlighter';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

export const EXPIRES_IN_OPTS = {
'3 months': `${3 * 30 * 24 * 60 * 60}s`,
'1 year': `${365 * 24 * 60 * 60}s`,
'100 years': `${100 * 365 * 24 * 60 * 60}s`,
};

const schema = z.object({
name: z.string().min(1).max(255),
expiresIn: z.string().optional(),
});

interface CreateTokenDialogProps {
Expand All @@ -33,6 +47,7 @@ export function CreateTokenDialog({
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
Expand Down Expand Up @@ -62,8 +77,6 @@ export function CreateTokenDialog({
);
}

// TODO: add a name for the token

return (
<DialogContent className="w-fit max-w-[80%] min-w-[500px]">
<DialogHeader>
Expand All @@ -90,6 +103,32 @@ export function CreateTokenDialog({
<div className="text-sm text-red-500">{nameError}</div>
)}
</div>
<Label htmlFor="expiresIn">Expires In</Label>
<Controller
control={control}
defaultValue={EXPIRES_IN_OPTS['100 years']}
name="expiresIn"
render={({ field }) => {
return (
<Select {...field}>
<SelectTrigger id="expiresIn">
<SelectValue
id="expiresInSelected"
placeholder="Select a duration"
/>
</SelectTrigger>
<SelectContent>
{Object.entries(EXPIRES_IN_OPTS).map(([label, value]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}}
/>

<Button disabled={props.isLoading}>
{props.isLoading && <Spinner />}
Generate token
Expand Down
5 changes: 4 additions & 1 deletion internal/services/webhooks/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ func (c *WebhooksController) check() error {

token = string(decTok)
} else {
tok, err := c.sc.Auth.JWTManager.GenerateTenantToken(context.Background(), tenantId, "webhook-worker")

expiresAt := time.Now().Add(100 * 365 * 24 * time.Hour) // 100 years

tok, err := c.sc.Auth.JWTManager.GenerateTenantToken(context.Background(), tenantId, "webhook-worker", &expiresAt)
if err != nil {
c.sc.Logger.Error().Err(err).Msgf("could not generate token for webhook worker %s of tenant %s", id, tenantId)
return
Expand Down
2 changes: 1 addition & 1 deletion internal/testutils/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func Prepare(t *testing.T) {
}
}

defaultTok, err := serverConf.Auth.JWTManager.GenerateTenantToken(context.Background(), tenantId, "default")
defaultTok, err := serverConf.Auth.JWTManager.GenerateTenantToken(context.Background(), tenantId, "default", nil)
if err != nil {
t.Fatalf("could not generate default token: %v", err)
}
Expand Down
26 changes: 16 additions & 10 deletions pkg/auth/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
)

type JWTManager interface {
GenerateTenantToken(ctx context.Context, tenantId, name string) (*Token, error)
UpsertTenantToken(ctx context.Context, tenantId, name, id string) (string, error)
GenerateTenantToken(ctx context.Context, tenantId, name string, expires *time.Time) (*Token, error)
UpsertTenantToken(ctx context.Context, tenantId, name, id string, expires *time.Time) (string, error)
ValidateTenantToken(ctx context.Context, token string) (string, error)
}

Expand Down Expand Up @@ -53,15 +53,15 @@ type Token struct {
Token string
}

func (j *jwtManagerImpl) createToken(ctx context.Context, tenantId, name string, id *string) (*Token, error) {
func (j *jwtManagerImpl) createToken(ctx context.Context, tenantId, name string, id *string, expires *time.Time) (*Token, error) {
// Retrieve the JWT Signer primitive from privateKeysetHandle.
signer, err := jwt.NewSigner(j.encryption.GetPrivateJWTHandle())

if err != nil {
return nil, fmt.Errorf("failed to create JWT Signer: %v", err)
}

tokenId, expiresAt, opts := j.getJWTOptionsForTenant(tenantId, id)
tokenId, expiresAt, opts := j.getJWTOptionsForTenant(tenantId, id, expires)

rawJWT, err := jwt.NewRawJWT(opts)

Expand All @@ -82,8 +82,8 @@ func (j *jwtManagerImpl) createToken(ctx context.Context, tenantId, name string,
}, nil
}

func (j *jwtManagerImpl) GenerateTenantToken(ctx context.Context, tenantId, name string) (*Token, error) {
token, err := j.createToken(ctx, tenantId, name, nil)
func (j *jwtManagerImpl) GenerateTenantToken(ctx context.Context, tenantId, name string, expires *time.Time) (*Token, error) {
token, err := j.createToken(ctx, tenantId, name, nil, expires)
if err != nil {
return nil, err
}
Expand All @@ -102,8 +102,8 @@ func (j *jwtManagerImpl) GenerateTenantToken(ctx context.Context, tenantId, name
return token, nil
}

func (j *jwtManagerImpl) UpsertTenantToken(ctx context.Context, tenantId, name, id string) (string, error) {
token, err := j.createToken(ctx, tenantId, name, &id)
func (j *jwtManagerImpl) UpsertTenantToken(ctx context.Context, tenantId, name, id string, expires *time.Time) (string, error) {
token, err := j.createToken(ctx, tenantId, name, &id, expires)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -196,8 +196,14 @@ func (j *jwtManagerImpl) ValidateTenantToken(ctx context.Context, token string)
return subject, nil
}

func (j *jwtManagerImpl) getJWTOptionsForTenant(tenantId string, id *string) (tokenId string, expiresAt time.Time, opts *jwt.RawJWTOptions) {
expiresAt = time.Now().Add(90 * 24 * time.Hour)
func (j *jwtManagerImpl) getJWTOptionsForTenant(tenantId string, id *string, expires *time.Time) (tokenId string, expiresAt time.Time, opts *jwt.RawJWTOptions) {

if expires != nil {
expiresAt = *expires
} else {
expiresAt = time.Now().Add(90 * 24 * time.Hour)
}

iAt := time.Now()
audience := j.opts.Audience
subject := tenantId
Expand Down
6 changes: 3 additions & 3 deletions pkg/auth/token/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestCreateTenantToken(t *testing.T) { // make sure no cache is used for tes
t.Fatal(err.Error())
}

token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token")
token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token", nil)

if err != nil {
t.Fatal(err.Error())
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestRevokeTenantToken(t *testing.T) {
t.Fatal(err.Error())
}

token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token")
token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token", nil)

if err != nil {
t.Fatal(err.Error())
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestRevokeTenantTokenCache(t *testing.T) {
t.Fatal(err.Error())
}

token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token")
token, err := jwtManager.GenerateTenantToken(context.Background(), tenantId, "test token", nil)

if err != nil {
t.Fatal(err.Error())
Expand Down
4D1F
3 changes: 3 additions & 0 deletions pkg/client/rest/gen.go

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

Loading
0