Skip to content

Commit

Permalink
Make API Keys not expire (#9)
Browse files Browse the repository at this point in the history
At some point, our API keys became the same as our user cookie auth, which means that by default, they expired after like an hour, which is a not particularly useful API key.

This PR moves the expiration to the year 9999, effectivelly creating a Y10K problem, but we'll cross that bridge when we get to it.

It also makes it easy if we want to allow configurable expirations in the future (e.g. versus removing `exp` entirely and having to handle that on the validation side).

Signed-off-by: Brandon Sprague <[email protected]>
  • Loading branch information
bcspragu authored Nov 7, 2023
1 parent e6e210c commit a0d3d26
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 15 deletions.
51 changes: 39 additions & 12 deletions cmd/server/usersrv/usersrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type Server struct {
// Exchange a user JWT token for an API key that can be used with other RMI APIs
// (POST /login/apikey)
func (s *Server) CreateAPIKey(ctx context.Context, req user.CreateAPIKeyRequestObject) (user.CreateAPIKeyResponseObject, error) {
tkn, id, exp, err := s.exchangeToken(ctx, false)
tkn, id, exp, err := s.exchangeToken(ctx, neverExpire)
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %w", err)
}
Expand All @@ -73,28 +73,55 @@ func (s *Server) CreateAPIKey(ctx context.Context, req user.CreateAPIKeyRequestO
}, nil
}

func (s *Server) exchangeToken(ctx context.Context, includeEmails bool) (string, string, time.Time, error) {
type exchangeTokenOptions struct {
includeEmails bool
neverExpire bool
}

type exchangeOption func(*exchangeTokenOptions)

func includeEmails(o *exchangeTokenOptions) {
o.includeEmails = true
}

func neverExpire(o *exchangeTokenOptions) {
o.neverExpire = true
}

func (s *Server) exchangeToken(ctx context.Context, opts ...exchangeOption) (string, string, time.Time, error) {
eOpts := &exchangeTokenOptions{
includeEmails: false,
neverExpire: false,
}
for _, opt := range opts {
opt(eOpts)
}
_, srcClaims, err := jwtauth.FromContext(ctx)
if err != nil {
return "", "", time.Time{}, fmt.Errorf("failed to get auth service JWT to exchange for service-issued JWT: %w", err)
}

var emails []string
if includeEmails {
if eOpts.includeEmails {
emails, err = emailctx.EmailsFromContext(ctx)
if err != nil {
return "", "", time.Time{}, fmt.Errorf("failed to get email from context: %w", err)
}
}

expC, ok := srcClaims["exp"]
if !ok {
return "", "", time.Time{}, errors.New("no 'exp' claim in source JWT")
}

exp, ok := expC.(time.Time)
if !ok {
return "", "", time.Time{}, fmt.Errorf("'exp' claim in source JWT was of type %T, expected a number", expC)
var exp time.Time
if eOpts.neverExpire {
exp = time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC)
} else {
expC, ok := srcClaims["exp"]
if !ok {
return "", "", time.Time{}, errors.New("no 'exp' claim in source JWT")
}
tmp, ok := expC.(time.Time)
if !ok {
return "", "", time.Time{}, fmt.Errorf("'exp' claim in source JWT was of type %T, expected a number", expC)
}
exp = tmp
}

sub, ok := srcClaims["sub"]
Expand All @@ -118,7 +145,7 @@ func (s *Server) exchangeToken(ctx context.Context, includeEmails bool) (string,
// Exchange a user JWT token for an auth cookie that can be used with other RMI APIs
// (POST /login/cookie)
func (s *Server) Login(ctx context.Context, req user.LoginRequestObject) (user.LoginResponseObject, error) {
tkn, id, exp, err := s.exchangeToken(ctx, true)
tkn, id, exp, err := s.exchangeToken(ctx, includeEmails)
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %w", err)
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/server/usersrv/usrsrv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ func TestLogin(t *testing.T) {
}

func TestCreateAPIKey(t *testing.T) {
srv, env := setup(t)
srv, _ := setup(t)

ctx := context.Background()
exp := env.curTime.Add(24 * time.Hour)
// API keys don't expire
exp := time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC)
tkn := jwt.New()
tkn.Set("sub", "user123")
tkn.Set("emails", []any{"[email protected]"})
Expand All @@ -73,7 +74,7 @@ func TestCreateAPIKey(t *testing.T) {
want := user.CreateAPIKey200JSONResponse{
Id: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75",
ExpiresAt: &exp,
Key: "eyJhbGciOiJFZERTQSIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsicm1pLm9yZyJdLCJleHAiOjEyMzU0MzE4OCwiaWF0IjoxMjM0NTY3ODksImp0aSI6IjZlNGZmOTVmLWY2NjItNDVlZS1hODJhLWJkZjQ0YTJkMGI3NSIsIm5iZiI6MTIzNDU2NzI5LCJzdWIiOiJ1c2VyMTIzIn0.2Xq5HOV9QYvSgI534oCPYzBvRH74f2Uek7tS04aXQ7YTUR_TKeyJkRyVp2AT3KfPh-aW38Lw-HAu3cR5cMAiBg",
Key: "eyJhbGciOiJFZERTQSIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsicm1pLm9yZyJdLCJleHAiOjI1MzM3MDc2NDgwMCwiaWF0IjoxMjM0NTY3ODksImp0aSI6IjZlNGZmOTVmLWY2NjItNDVlZS1hODJhLWJkZjQ0YTJkMGI3NSIsIm5iZiI6MTIzNDU2NzI5LCJzdWIiOiJ1c2VyMTIzIn0.sf7SbHOWGvW3mHadEsz64penWakt6KtlAs6z6EyYKcQRIiHeqMmoN6nycYnFjQ1RxD22IytFUDi_45Udi6UQCg",
}

if diff := cmp.Diff(want, got); diff != "" {
Expand Down

0 comments on commit a0d3d26

Please sign in to comment.