问题
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.
回答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.
- When an API account is provisioned generate 2 random values: a Token and a Secret.
- 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
- 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");
回答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 likelogged_in = TRUE
. - Now, every other request sent by the user should include a
token
field (either as aPOST
field or aGET
parameter). At this point, I would re-consider using REST and only usePOST
requests for everything, with theoperation
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
andlogged_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.
回答3:
The points in your plan are essentially the basic features of OAuth. It depends on your requirements. If your APIs are for internal use only, you could send a secret key with HMAC-SHA authentication.
回答4:
Well you wrote a lot how to invent cookies from the scratch and how to store them in the database.
In the same time you already have usernames, passwords and HTTPS for security data transfer. Why won't you use just cookies?
来源:https://stackoverflow.com/questions/46719676/implementing-simple-authentication-for-php-rest-api