11436 SSO

Data-Oriented Designs for Common API Problems

Mnally Mgardiner
Jan 06, 2016

We recently received an excellent post on the Apigee Community from Matt Miller regarding the design of specific data-oriented REST APIs. Matt posed this question: what do you do with API functionality that doesn’t correspond to traditional CRUD operations in obvious ways?

He then offered the following example problems:

  • a service that receives user credentials and responds with an error or a token
  • a service that emails a user with a special token for resetting his/her password
  • a service that receives a token and resets a user's password
  • a service that refunds a purchase (but doesn't delete an Order record, of course!)
  • a service that expires the user's token (log out)

Matt subsequently proposed some solutions of his own, but we liked his example problems so much we thought we'd offer our own solution ideas. We’ve wrestled with some of these problems ourselves.

The log in/log out problem

In general, we find it easiest to design solutions in a REST style if we start with the question "what are the entities and what do they look like?" We think the log in/log out example fits the data-oriented model in a fairly obvious way, using a design that is fairly close to the one Matt suggested. We think of this problem as being simply the creation and deletion of “session” resources. A session resource is created using:

POST /sessions HTTP/1.1
Host: example.org
Content-Type: application/json
Content-Length: nnnn

{"kind": "Session", "userid": "user", "password" : "password"}

HTTP/1.1 201 Created
Location: https://example.org/sessions/xxxxx

xxxxx is the value that we would think of as the token in the service-oriented design style that’s popular for APIs. In our data-oriented design, the token is a full URL. A logout is simply a DELETE of the URL https://example.org/sessions/xxxxx.

We confess that the last time we implemented a solution to this problem, we didn't do it this way—we used a POST for logout—but we can't think of a good reason now why we did that.

If you are using this API for a browser client, you may also want to use a set-cookie header in the response so that the browser will automatically include the session URL in future calls. A DELETE might set the cookie to null.

Note that this design does not require you to actually store a session object, either on disk or in memory—it is sufficient to use the URL as an identifier. Whether you choose to implement a GET on the session URL is really up to you. The typical use-cases don’t require it, but, being fastidious, we would probably implement it anyway for completeness in order to provide a simple client test for the validity of a token-URL and possibly to provide information on session timeouts and other properties.

A minor downside of this design is that the token is a bit bigger than it would have been if it had been only xxxxx. If that bothers you, then you could stick to the data-oriented CREATE/DELETE model for sessions, but also publish the URL template https://example.org/sessions/{}. This creates a bit more coupling between the client and the server, which is in general undesirable, but it allows you to pass back and forth smaller tokens. When the client wants to do the logout, it will use the URL template to turn the short token into a URL on which it can perform a DELETE.

In order for client programmers to use this API, they’ll have to know the representation of a session (you always have to learn these in data-oriented APIs) and the URL to POST to. This URL could simply be a well-known URL, or it could be discovered using this request:

GET /sessions HTTP/1.1
Host: example.org
Accept: application/json

HTTP/1.1 200 OK
{"kind": "Site", "sessions": "https://example.org/sessions"}

If you take the latter route, then you only need one well-known URL for your site, (https://example.org/), but we think it's fairly harmless to also make https://example.org/sessions well-known, too.

The password reset problem

For Matt's password reset example, we would start again by defining the resources. We think userid and password are best thought of as properties of a “profile” or “account” resource. It is very common to name this resource "user," and LDAP terminology encourages this naming, but it doesn't take much thought to see that you aren’t really modeling the user, you’re just modeling the user's relationship with your system, so a name like profile or account is a better fit. (If this isn’t immediately obvious, consider the fact that an LDAP user has a single userid and password, and then ask yourself how many userids and passwords you have).

If userid and password are properties of a profile resource, then a change of password is most obviously modeled as a PATCH of a single property of that resource. Setting a password would look like this:

PATCH /profiles/yyyyy HTTP/1.1
Content-Type: application/merge-patch+json
Content-Length: mmm

{"password": "new-password"}

HTTP/1.1 200 OK

application/merge-patch+json is the simplest media type for patching JSON. It is documented at https://tools.ietf.org/html/rfc7386.

In the common case, PATCH happens in the context of a session that was established by a user by logging in. The session ID (a URL if we are following the pattern above) will flow from the client to the server in a header—either the authorization header or a custom header for API clients, or a cookie for browser clients.

In the case of a password, despite the protests of security purists, we often have to allow a password to be set without a valid session because the user has forgotten the current password. Thinking of the problem this way helps us understand what the "special token" is that gets emailed to a user for password reset: it’s the identifier of a temporary session that only allows setting a password.

Generally speaking, when a password-reset link is emailed to a user, the intention is that the user will click on the link and end up in a UI that enables the setting of a new password. If we were implementing this today, we might make the URL look like this:

https://example.org/profiles/987654?session=zxcvbnm#password

Of course this URL is opaque to clients—only the server will interpret it and only the server needs to know what it looks like, so the server can change its format at any time. https://example.org/profiles/987654?session=zxcvbnm is an alias for https://example.org/profiles/987654—they both reference the same resource, but the server will use the session ID in the query string of the first URL instead of looking for the session ID in headers as it would for the second.

Clicking on this URL will cause the browser to send a GET request with an accept header value indicating that it comes from a browser that would prefer HTML. The HTML rendering is the same as for https://example.org/profiles/987654. In our HTML rendering of this profile resource, we would include JavaScript code that looks at the fragment identifier and does the right thing. The fragment identifier indicates that only the password was referenced. The JavaScript UI code would show the UI for a password alone, which would enable the user to set a new password.

Matt's refund example looks straightforward to us. We would just implement a refund resource and let people POST to /refunds to create them. The representation of a refund would reference the order for which it is a refund or a subset of the line-items within the order. Perhaps we're missing a subtlety on this one.

Any ideas or questions you might have would be a great addition to the discussion Matt started. Join the conversation on the Community. Thanks, Matt!

Microservices Done Right

Next Steps

 
 

Resources Gallery

News