Implementing simple authentication for PHP REST API

36,797

Solution 1

One of the major points of REST as a concept is to avoid the use of session state so that it's easier to scale the resources of your REST endpoint horizontally. If you plan on using PHP's $_SESSION as outlined in your question you're going to find yourself in a difficult position of having to implement shared session storage in the case you want to scale out.

While OAuth would be the preferred method for what you want to do, a full implementation can be more work than you'd like to put in. However, you can carve out something of a half-measure, and still remain session-less. You've probably even seen similar solutions before.

  1. When an API account is provisioned generate 2 random values: a Token and a Secret.
  2. When a client makes a request they provide:
    • The Token, in plaintext.
    • A value computed from a unique, but known value, and the Secret. eg: an HMAC or a cryptographic signature
  3. The REST endpoint can then maintain a simple, centralized key-value store of Tokens and Secrets, and validate requests by computing the value.

In this way you maintain the "sessionless" REST ideal, and also you never actually transmit the Secret during any part of the exchange.

Client Example:

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));
$stamp  = "2017-10-12T23:54:50+00:00";        // date("c");
$sig    = hash_hmac('SHA256', $stamp, base64_decode($secret));
// Result: "1f3ff7b1165b36a18dd9d4c32a733b15c22f63f34283df7bd7de65a690cc6f21"

$request->addHeader("X-Auth-Token: $token");
$request->addHeader("X-Auth-Signature: $sig");
$request->addHeader("X-Auth-Timestamp: $stamp");

Server Example:

$token  = $request->getToken();
$secret = $auth->getSecret($token);
$sig    = $request->getSignature();

$success = $auth->validateSignature($sig, $secret);

It's worth noting that if decide to use a timestamp as a nonce you should only accept timestamps generated within the last few minutes to prevent against replay attacks. Most other authentication schemes will include additional components in the signed data such as the resource path, subsets of header data, etc to further lock down the signature to only apply to a single request.

2020 Edit: This is basically what JSON Web Tokens [JWT] are.

When this answer was originally written in 2013 JWTs were quite new, [and I hadn't heard of them] but as of 2020 they've solidly established their usefulness. Below is an example of a manual implementation to illustrate their simplicity, but there are squillions of libs out there that will do the encoding/decoding/validation for you, probably already baked into your framework of choice.

function base64url_encode($data) {
  $b64 = base64_encode($data);
  if ($b64 === false) {
    return false;
  }
  $url = strtr($b64, '+/', '-_');
  return rtrim($url, '=');
}

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));

// RFC-defined structure
$header = [
    "alg" => "HS256",
    "typ" => "JWT"
];

// whatever you want
$payload = [
    "token" => $token,
    "stamp" => "2020-01-02T22:00:00+00:00"    // date("c")
];

$jwt = sprintf(
    "%s.%s",
    base64url_encode(json_encode($header)),
    base64url_encode(json_encode($payload))
);

$jwt = sprintf(
    "%s.%s",
    $jwt,
    base64url_encode(hash_hmac('SHA256', $jwt, base64_decode($secret), true))
);

var_dump($jwt);

Yields:

string(167) "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IkJtbjBjOHJRREpvR1RpYmsiLCJzdGFtcCI6IjIwMjAtMDEtMDJUMjI6MDA6MDArMDA6MDAifQ.8kvuFR5xgvaTlOAzsshymHsJ9eRBVe-RE5qk1an_M_w"

and can be validated by anyone that adheres to the standard, which is pretty popular atm.

Anyhow, most APIs tack them into the headers as:

$request->addHeader("Authorization: Bearer $jwt");

Solution 2

I would say you should generate a unique token and use that for communication. Basically:

  • The client sends username/password to the login resource.
  • The server verifies the username/password combination. If it's correct, it generates a unique toquen, saves it in the sessions table and sends it back to the user, along with a status update like logged_in = TRUE.
  • Now, every other request sent by the user should include a token field (either as a POST field or a GET parameter). At this point, I would re-consider using REST and only use POST requests for everything, with the operation as a POST field. That would not add the token to the URL and, thus, letting it be registered on a web browsing historial, routers and stuff.
  • On every request, the server should check if the token exists and it's valid. If not, simply return an error message like 403 Forbidden and logged_in = FALSE.

The system could also require to send another data to make it more secure, like a client-generated unique id and stuff like that, which should be sent with the token and checked server-side.

Share:
36,797

Related videos on Youtube

Nils
Author by

Nils

Updated on January 03, 2020

Comments

  • Nils
    Nils over 4 years

    I am working on adding a REST API to a legacy PHP site. This is to provide an endpoint for an internal app, so I am quite free in how I design things and what do and don't support.

    What I now need to add to this API is a way to login, and then perform actions as a specific user. The site has been built years ago and not necessarily with the best practices at the time, so I am unfortunately a bit restricted in how I do this. All of this needs to run in PHP 5.4 with MySQL 5.6.

    I have been reading up on common designs for this and OAuth1/2 looks like the most common standard. However, this seems like massive overkill for my purposes, since it has various features that I do not need and seems very complicated to implement.

    Instead, I am planning on just doing something like this:

    • The client calls a get_session API endpoint, which generates a random session ID, saves that to a table in the database and returns it to the client.
    • The client saves this session ID.
    • Then the client authenticates by sending a request to the login endpoint, sending the username, password and session ID (via HTTPS obviously).
    • The server compares the data to the user table and, if the login is correct, updates the session table to associate the session ID with the corresponding user ID. This needs to be rate-limited in some way to prevent brute forcing.
    • Now the client can call any other endpoints providing only its session ID for authorization.
    • On each request, the server looks up the session ID, sees which user it has been associated with and performs the correct action.
    • The client can remember the session ID for future use, until it either gets removed manually or expires after some amount of time.
    • To log out, the client sends a request to the logout endpoint and the server removes the association with the user account.

    Is this a reasonable design? It's obviously not very sophisticated, but I am looking for something that I can implement without a huge hassle or requiring third-party libraries.

    • odan
      odan over 6 years
      Just use Basic access authentication and HTTPS.
    • Konrad Rudolph
      Konrad Rudolph over 6 years
      And as usual downvoters should leave explanatory comments. For what it’s worth this seems like a well-researched, detailed question. I also don’t agree that it’s “primarily opinion based”. There’s certainly a set of best practices that a good answer would refer to (as Sammitch has done).
  • Nils
    Nils over 6 years
    My impression was that cookies are not a good idea to use for a REST API. I'm not even sure if the framework I am using for the app supports them.
  • Nils
    Nils over 6 years
    So you mean I would essentially skip the first step (get_session) and simply generate a session ID/token once login is successful? That's a good point. However, I was hoping to use the get_session mechanism to keep track of the number of login attempts to prevent brute forcing, so I'd need to find an alternative for that.
  • Alejandro Iván
    Alejandro Iván over 6 years
    Yes, you could use a cookie for that. You should read on how the CodeIgniter framework does it. It basically generates a request/session token (basically a string) and that's the whole data that's sent to the user as a cookie. Then it uses it to read a register from the database and get the whole session data (so no sensitive data is sent to the user). Usually apps let you configure cookie data when you do HTTP operations (NSHTTPCookieStorage on iOS for example, I don't know on Android but it should be similar).
  • user1597430
    user1597430 over 6 years
    Cookies are just part of your HTTP(S) request. It's a line (or even some lines) in the header request. You may call it what you want but your "session" mechanism in the first post it's a cookie.
  • Sammitch
    Sammitch about 6 years
    I would say that that error was ( •_•) / ( •_•)>⌐■-■ / (⌐■_■) @HardlyNoticeable. YEEEEEAAHHHHHHHHH
  • Sammitch
    Sammitch over 4 years
    Aww, I overwrote @HardlyNoticeable's edit and ruined my dumb joke. T_T