What are the pros and cons of authentication with API key vs client_id+secret?
I have an implementation for an internal API, the requirement is to implement some sort of basic authentication instead of oauth (generating a token).
Do you think there's any difference between using just an API key vs using a client id + secret?
For what I see it'd be just like saying "using a password" vs "using a user and a password".
I'd recommend having a username/client id just because that way if/when you have multiple clients, they won't all be sharing a common secret key. Also, you can have different sets of permissions for different clients.
Also, it's important for APIs to take steps to mitigate replay attacks.
Basically, the idea is that you have a secret key that both the server and client know. The client includes a "token" with each request. The token is generated by appending a current timestamp to the secret key, hashing the result, and then appending the client id and the same timestamp to the secret key. The server checks that the timestamp is within a certain amount of time ago and if not rejects the request. (Say, 30 seconds. This does require that the client and server's clocks are synced, but that's usually not an issue in today's world.) The server then uses the client id to look up the secret key for that particular client, appends the timestamp provided, hashes the same way the client did, and checks that the hashes match. If they don't the server rejects the request.
Example:
The folks on the server side provide to the client a client id and secret key and save a copy of it in the server's config. I'll use "client_1" and "53cr37" as examples, but for a real case, the secret key ought to have about 256 bits of entropy.
The client gets a current timestamp. I'll use "1692640316"
The client appends the timestamp to the secret key. "53cr37:1692640316"
The client hashes the result. I'll use base16-encoded sha256 for this example, but in the real world, I'd probably go for base64-encoded sha512. "63e9a1e333282b89776962b3f17e20b7fce7a0d5735c013dffc85abd5e964850"
The client then appends the client id and timestamp to the hash: "63e9a1e333282b89776962b3f17e20b7fce7a0d5735c013dffc85abd5e964850:client_1:1692640316"
The client submits this string to the server in a header with their request. "Authorization: TOKEN 63e9a1e333282b89776962b3f17e20b7fce7a0d5735c013dffc85abd5e964850:client_1:1692640316"
The server receives the request, gets a current timestamp, and compares it against the timestamp at the end of that string. If the timestamp in the request is more than 30 seconds different from the server's current time, the server rejects the request.
The server looks up the secret key ("53cr37") for the client "client_1", appends the timestamp, hashes the result, and compares it to the hash in the request. The server then honors the request only if the hashes match.
The reason why this mitigates replay attacks is the 7th step there. If one hash gets intercepted, a bad actor can wreak havoc for 30 seconds (or, rather, however long you tune it to, balancing security concerns with any risks involved with things breaking because of clock drift or latency), but no more. With just a client id and secret key, the bad actor could wreak havoc until the dev team managed to notice something was up and change the secret key.
The reason the timestamp has to be included in the hashed value is because if only the secret key was included, the hash would never change. Then if the hash was intercepted once, you'd be back to the situation of a bad actor wreaking havoc until the dev team manually changed the secret key.
Also, given that you're hashing the secret key, the secret key isn't in the request and cannot be determined from intercepted requests.
(Of course, if a bad actor roots a box and is able to get the secret key value itself, they can then generate valid hashes/tokens to their heart's content until the dev team changes the secret key, but that's not the sort of thing this authentication scheme is meant to protect against.)
Edit: One more thing to mention here. Keep in mind that it's not going to be terribly easy to go changing your authentication method later. If you want to change how authentication works, you'll have to go to all clients and get them to change how they make requests. This is one of those cases where futureproofing is warranted. Better to do it "right" and with features that should mostly work for your purposes for the foreseeable future. Even if you don't know that you'll have multiple clients right now, it's good to plan as if you might some day.
Also, aside from the security implications related to having client ids in the requests, the client id can also be used to track things like resource usage or TPS on a per-client basis. Which is really handy when your app is overwhelmed at 3:00am and you need to tell the client to ease off. You'll only know which client to talk to if you can track that kind of information on a per-client basis.
I'd consider it a concern if I was in your shoes, yes. Mostly for the reasons jflorez mentioned.
Major security breaches wherein an attacker gained has access to an internal, private network happen not infrequently. Target (the retail chain) leaked a ton of their customers' credit card to attackers over the course of (IIRC) months. The attackers couldn't have done it (at least not in the way they did) without first breaching Target's private corporate network.
Never underestimate the risk of an attack coming from the inside.
Also once you have an implementation with a certain kind of authentication other devs are likely to copy what you have successfully deployed and then your security assumptions will make it into public facing code without much consideration
The difference is that you can have multiple API keys for the same account.
You can revoke API keys from a lost device without changing your password.
You can grant a different service a restricted API key for limited access.
API keys can expire, forcing password expiry is very use unfriendly.
The password is the "root secret" of the account. It is (mostly) unrevokable and doesn't expire. It is a huge risk to have the password lying around. So it is better to quickly exchange the password for a less risky token, then you can wipe the password. Then all clients don't need to store the password. The user just needs to provide it once then lower-value secrets can be used for future authentication.
I don't fully understand what use case you're thinking about.
An API key which expires is very hard to work with, imagine deploying an app with that kind of key, or a service/bot which uses that key.
Maybe you're thinking about access tokens, which need to be regenerated every so often and can be generated with a refresh token.
API tokens don't need to expire. But having them expire is a nice security benefit. It means that eventually they will be useless even if they are sitting around on an old laptop that gets thrown away.
For many use cases this works well. For example a client app can easily refresh the token from time-to-time. This can be managed with an access/refresh token system (which has the advantage that the token you are frequently sending over the wire doesn't have refresh capabilities)
You got me thinking in something more, are API keys stored in plain text in DB? Otherwise I don't see a way to quickly know it's valid, I'd have to validate it against all the hashes in the DB.
With client id it'd be easy to just validate the secret against a single hash for that user.
There are lots of solutions out there for "secrets management." If you're using Kubernetes, there are some which integrate with Kubernetes. I use Spring Cloud Config where I work and it supports storing encrypted values in the configuration. What solution would be best for you depends on your software stack. (And I don't have a ton of experience with most options.) But some googling could get you more answers.
Consider that a 'username+password' is much harder to 'revoke' individually. As in, you can have 3-4 API keys in use, and can revoke any one of them without having to change a password.
You can also change password independently of the keys, or have it linked so keys are revoked on a password change. It also allows traceability as to where accesses are coming from (auditability). If everything is using the same client-id+secret (or usn/pwd), you don't know which 'client' is doing what.
Yeah, mentioning password it was just an analogy, the user has their credentials independent of this implementation, so no need to reset their password for any flow here. It'd be client id+secret.
It's fine as long as the key/secret is never transmitted in clear text (always encrypted e.g. with https) and never exposed to the end users to prevent credential leak. What matter is if you can rotate those keys quickly enough when there is a security incident. oauth has advantage here because the token has expiry date so if you happen to have a leak, at least the leaked token won't work indefinitely.
For what I see it'd be just like saying "using a password" vs "using a user and a password".
As long as API keys have more entropy than typical username & password combinations they can be more secure. Imagine if you had a system where you make a token by concatenating username and password - the security properties don't change just because you're exchanging one string instead of two separate ones.