2

We're building a new REST API and have a few decisions to make regarding the primary key and idempotency_key. Our API has many users and users are only allowed to lookup objects that belong to them. Requests are authenticated so we know the userId associated with a given request.

Properties of concern:

  • userId The id of the user.
  • idempotenyKey The idempotency key used in idempotent requests to the API. Unique per user.

Let's suppose there is a hypothetical table Cars with endpoint GET .../cars/{id} which we'll base our discussion on.

Options

Option 1 - Use composite of userId and idempotencyKey as primary key

CREATE TABLE Cars (
 userId VARCHAR(255) NOT NULL,
 idempotencyKey VARCHAR(255) NOT NULL,
 description VARCHAR(255),
 PRIMARY KEY (userId, idempotencyKey),
 INDEX user_index (userId)
);

Item Lookup: GET .../cars/{idempotencyKey}

Thoughts:

  • The id used in lookups is guessable.
  • Since objects are authenticated this doesn't appear to pose an immediate security concern, but it does mean if ever there is an endpoint in the API that allows our user's user to look something up, we need to assume the lookups are guessable.

Option 2 - Using a hash of the userId + idempotencyKey as the primary key

CREATE TABLE Cars (
 id VARCHAR(255) NOT NULL,
 userId VARCHAR(255) NOT NULL,
 idempotencyKey VARCHAR(255) NOT NULL,
 description VARCHAR(255),
 PRIMARY KEY (id),
 INDEX user_index (userId)
);

When inserting we'd calculate id = hash(userId + idempotencyKey).

Item Lookup: GET .../cars/{id}

Thoughts:

  • Primary keys are unguessable.
  • This further prevents the possibility of security risks on operations against ids.
  • No longer a composite primary key.
  • Unlike option 3, doesn't require a unique constraint on (userId, idempotencyKey).

Option 3 - Generating a uuid primary key

CREATE TABLE Cars (
 id VARCHAR(255) NOT NULL,
 userId VARCHAR(255) NOT NULL,
 idempotencyKey VARCHAR(255) NOT NULL,
 description VARCHAR(255),
 PRIMARY KEY (id),
 INDEX user_index (userId),
 CONSTRAINT unique_by_user UNIQUE (userId, idempotencyKey)
);

When inserting we'd set id = UUID.randomUUID().

Item Lookup: GET .../cars/{id}

Thoughts:

  • id is totally unguessable and isn't tied to any business data.
  • Requires an extra index.

Option 4 - auto incremented primary key that's never exposed to the user

CREATE TABLE Cars (
 id INT NOT NULL AUTO_INCREMENT,
 userId VARCHAR(255) NOT NULL,
 idempotencyKey VARCHAR(255) NOT NULL,
 description VARCHAR(255),
 PRIMARY KEY (id),
 INDEX user_index (userId),
 CONSTRAINT unique_by_user UNIQUE (userId, idempotencyKey)
);

There're a few ways to do a lookup. Lookup could be based solely on idempotencyKey, ie: GET .../cars/{idempotencyKey}

Alternatively, we could autogenerate a referenceId uuid which would be indexed and used for lookups.

Thoughts:

  • The primary key is never exposed to the user which is a pretty common practice.
  • Allows for more flexibility if business logic changes (though for fundamental concepts like userId and idempotencyKey, I can't see how these would change).
  • The primary key is an INT and is thus smaller. This allows for smaller indexes and arguably more performance.
  • Again requires an extra index.

Looking for advice on what approach to take. Considerations around performance and scale are very important.

Thanks!

TimJ
  • 426
  • 4
  • 12
  • The way I handle something like this is to send a user ID in the `Authorization` header (the user ID will be a claim within the JWT in the Authorization header), and then use that to compare against the user ID in the request path, e.g. `/user/1`. If they match, then the user authenticated can act on behalf of the user in the request path. This is very secure because the user ID in the Authorization header can never be modified as long as no one has your secret key. Just keep in mind the JWT could be used maliciously, so only do this if a user is acting on themself, not someone else. – Lansana Camara May 16 '18 at 18:54
  • Does the user (i assume you have a user table) table also have a column that has the idempotency_key? If so you are storing the same data double and you should remove the idempotency_key from the car table .. You can simply find a idempotencyKey relation between a user and a car with this query `SELECT * FROM user INNER JOIN car ON user.id = car.userId WHERE user.idempotencyKey = "key"` – Raymond Nijland May 16 '18 at 19:18
  • For extra security you can add a restriction from what ip addresses the idempotencyKey can be used. – Raymond Nijland May 16 '18 at 19:22
  • For authentication, we use a JWT with the userId in the payload. I'm more interested in feedback on the possible primary key choices regarding the database schema. @RaymondNijland, regarding the user table, we may as well assume that one exists and that there is a foreign key constraint on the userId property on cars. It's not possible to drop idempotencyKey from `Cars` though since a user can have many cars and idempotency must be enforced for requests to create cars. – TimJ May 16 '18 at 20:37

0 Answers0