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:
get_session
API endpoint, which generates a random session ID, saves that to a table in the database and returns it to the client.login
endpoint, sending the username, password and session ID (via HTTPS obviously).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.
Client Credentials Flow First, turn on the Client Credentials grant on then Advanced settings > Grant Types tab on the Application settings page. Next, authorize the Application for the API being used on the Machine to Machine Applications tab on the API's Settings page.
OAuth (specifically, OAuth 2.0) is considered a gold standard when it comes to REST API authentication, especially in enterprise scenarios involving sophisticated web and mobile applications. OAuth 2.0 can support dynamic collections of users, permission levels, scope parameters and data types.
Once the user has filled in a username and a password, the URL containing the PHP script will be called again with the predefined variables PHP_AUTH_USER , PHP_AUTH_PW , and AUTH_TYPE set to the user name, password and authentication type respectively. These predefined variables are found in the $_SERVER array.
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.
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.
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");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With