mevdschee / php-crud-api

Single file PHP script that adds a REST API to a SQL database

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JWT with Firebase for secure authentication

kildos opened this issue · comments

Hello! First of all, I want to thank you for your great work on this API, @mevdschee. I've used it in a couple of projects, and it has worked seamlessly.

Now, let's get to the point. I want to use Firebase for user authentication, but there's something I don't quite understand how it works. If the 'secrets' part use obtained data from the Google public keys at:

//code to fetch jwt key and secret
$rawPublicKeys = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com');
$keys = json_decode($rawPublicKeys, true);
$keyKidsArray = array_keys($keys);
$pKeysArray = array_values($keys);
$secrets = $keyKidsArray[0] . ':' . $pKeysArray[0] . ',' . $keyKidsArray[1] . ':' . $pKeysArray[1];

as suggested in this Issue (#708).

How does this API verify that a user has logged into my legitimate app (and not other) if it only checks public keys? In other words, if someone creates a new Firebase project and adds my email, and that person logs in into his project with my email, he would obtain a valid Firebase token, so...could they call my API with that token, and would the middleware allow the access since it's a valid token for Firebase? My question is: where does the API supposedly verify some kind of private key?

I feel like I'm missing something, and I'm a bit confused about this.

Thanks again for your work, @mevdschee ❤️

That's the point about JWT: it's not about granting access; it's about authentication. Once uniquely authenticated, the granting process should be implemented at the application level. For example, by creating a local account with the sub-contained data or matching the user with an existing local account. Logging out is also not straightforward. That's why JWT tokens have a very short lifespan, and strategies like key rotation or refresh tokens are used to periodically validate who has access and who does not.

A JWT token consists of a header, a payload, and a signature. With each request, you can verify the payload content and match it with your authorized users. If they are not your users, you can register them in your database or remove them if necessary.

to check the payload u can enable the authorisation middleware

...
'middlewares' => 'cors,json,jwtAuth,authorization,customization',
...

and then

...
'authorization.recordHandler' => function ($operation, $tableName) {
            if($tableName === '<YourSensibleTable>')
                return 'filter=<userIdField>,eq,'.$_SESSION['claims']['sub'];
            return false;
        },
...

Thank you for your response @nik2208, I really appreciate it. I am a mobile developer with no deep experience on the backend part so I apologize in advance for any concept error that I may say.

My idea was precisely what you mentioned: to use the 'claims' that come in the 'payload' part of the token to securely authenticate who is accessing my API. However, I still don't understand how the API ensures that this token belongs to my Firebase project and not someone else's. If someone knew that steve.jobs@apple.com was registered in my database, that person could create a project in Firebase, create a fake user with that email, and obtain a valid token. Then, if that person sent a request to my API with that token, my API would let them in and believe it's the real steve.jobs@apple.com, because the 'claims' would contain that email. Am I wrong?

Thanks a lot for your help

U usually share/set the secret key with/on the issuer (firebase in the case) relevant setup page, if u can actually verify the signature (keys signed with other audience's keys would eventually fail when attempting to verify them with your secret key, does it sound?) it means that that particular token has been issued using your keys and not others. You would have access to the claims anyway, whether the jwt signature verification process fails or succeeds.

@nik2208
I've been thinking that I could address my concern by having the API check that the 'aud' claim matches my Firebase project's ID. This way, I would ensure that the token comes from my actual project. This simple check could be added in the authorization.recordHandler as well, right? Thank you!

well, I don't know how private is your firebase project Id, the shared secret key should be the way to do it.

What's the content of the decoded jwt token (paste it here) u get from firebase?

@nik2208

well, I don't know how private is your firebase project Id, the shared secret key should be the way to do it.

But even if my project ID was leaked, no one could ever create a valid token with the 'aud' claim of my ID project, because they can't sign valid Firebase tokens without the Firebase private key. And firebase generates that token based on the authentication. Right?

What's the content of the decoded jwt token (paste it here) u get from firebase?

{
"alg": "RS256",
"kid": "f2e82732b971a135cf1416e8b46dae04d80894e7",
"typ": "JWT"
}

{
"iss": "https://securetoken.google.com/sic-poc",
"aud": "sic-poc",
"auth_time": 1697220761,
"user_id": "fXJ3TTQAYRZ2NF5qvXlHUt2vOXC3",
"sub": "fXJ3TTQAYRZ2NF5qvXlHUt2vOXC3",
"iat": 1697220761,
"exp": 1697224361,
"email": "kildos@kildos.com",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"kildos@kildos.com"
]
},
"sign_in_provider": "password"
}
}

but I can generate a jwt token containing this exact payload (no need to be firebase the issuer). The only tool u can make use of is the signature verification, that states that the token has been issued and sealed using ur key, that supposedly is owned only by you and firebase.

@nik2208 But that check is already done when using the jwtAuth and authorization middlewares, isn't it? I mean, currently I use this on my config, that (at least it is what I understand) check that the token is signed by Firebase, so even if you send me a token with that payload, the check would not work, isn't it?

     //code to fetch jwt key and secret
$rawPublicKeys = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com');
$keys = json_decode($rawPublicKeys, true);
$keyKidsArray= array_keys($keys);
$pKeysArray = array_values($keys);
$secrets = $keyKidsArray[0] . ':' . $pKeysArray[0] . ',' . $keyKidsArray[1] . ':' . $pKeysArray[1];


$config = new Config([
    'driver' => 'mysql',
    'address' => 'localhost',
    'port' => '3306',
    'username' => 'test',
    'password' => 'passTest',
    'database' => 'testDB',
    'middlewares' => 'cors, jwtAuth, authorization',
    'jwtAuth.secrets' => $secrets,
    'cors.allowedOrigins' => '*',
    'cors.allowHeaders' => 'X-Authorization',
    'debug' => true
]);

If I am missing something I apologize, I try to understand how can I check the signature verification on this API, like you said. Thanks again

are u manually fetching the jwt token from firebase?

@nik2208 Yes, my app calls the native Firebase iOS SDK to do the login, and if the login is successful, I call the getIDTokenResult of their SDK to get the token. Then, I use that token to call this API

your app should store the received token in the headers, now instead, if I understand the code above, ure actually using the jwt code as secret, when the secret should be the same key you stored in firebase to sign the token

your requests should look like this:
Screenshot 2023-10-13 at 21 14 03
the jwt middleware then reads the token from the header and decodes it

have a read here #926

@nik2208
Thank you for your help again. The flow you mention is the one I currently follow: once I obtain the Firebase token, my app starts calling api.php, sending the token obtained from Firebase in the 'X-Authorization' header for each call.

With the jwtAuth and authorization middlewares, I thought what api.php did was to take the token provided in the header and try to see if it's signed with the key passed in the 'secrets' variable, which is what I currently get by doing what I mentioned before:

 //code to fetch jwt key and secret
$rawPublicKeys = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com');
$keys = json_decode($rawPublicKeys, true);
$keyKidsArray= array_keys($keys);
$pKeysArray = array_values($keys);
$secrets = $keyKidsArray[0] . ':' . $pKeysArray[0] . ',' . $keyKidsArray[1] . ':' . $pKeysArray[1];
$config = new Config([
    'driver' => 'mysql',
    'address' => 'localhost',
    'port' => '3306',
    'username' => 'test',
    'password' => 'passTest',
    'database' => 'testDB',
    'middlewares' => 'cors, jwtAuth, authorization',
    'jwtAuth.secrets' => $secrets,
    'cors.allowedOrigins' => '*',
    'cors.allowHeaders' => 'X-Authorization',
    'debug' => true
]);

Does api.php work different from what I've just said?

I am not using the jwt code as the secret. The https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com returns the current public keys for Firebase, which are refreshed fairly often, so that's why I added the function to retrieve them. So, I understand that api.php checks if those public keys matches the X-Authorization token provided, and if it matches, it means that it was created using the private key stored in Firebase.

Am I wrong on this? If so, what should my jwt.secrets contain?

I had previously read the other issue you mention (#926) before creating this issue, but considering that Firebase fully handles my authentication, I believe I don't need anything more than this api.php, right? I understand that my doubt is about how to place my private key in api.php, which was really what motivated me to create this issue.

maybe I got lost.. are u able to login? does everything work properly? ur doubt is about how to grant access properly?
where and when is this code

 //code to fetch jwt key and secret
$rawPublicKeys = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com');
$keys = json_decode($rawPublicKeys, true);
$keyKidsArray= array_keys($keys);
$pKeysArray = array_values($keys);
$secrets = $keyKidsArray[0] . ':' . $pKeysArray[0] . ',' . $keyKidsArray[1] . ':' . $pKeysArray[1];

executed?

furthermore: is firebase implementing key rotation or refresh token?

@nik2208
Sorry, maybe my wording caused some confusion.

are u able to login? does everything work properly?

Yes, everything works properly, I am able to login and everything works as expected, but I was concerned about the possibility that someone could enter my API using another Firebase project to obtain a valid 'Firebase-signed' jwt token. But after researching, I think that if api.php checks if the token provided matches the public keys of Firebase, and if so, then check some data in the claims, such as the project ID (which is unique), I think it would be safe.

where and when is this code

I added this code in the api.php, so the secrets (public keys from Firebase) that the config of api.php use are retrieved in runtime, because Firebase refreshes the token regularly. At the end, I do not know the private keys of Firebase so I could not add those anywhere.

Thank you @nik2208 for explaining the concepts to @kildos. @kildos thank you for your kind words on the project, I'm glad you like it.

I've been thinking that I could address my concern by having the API check that the 'aud' claim matches my Firebase project's ID.

Yes, 'api audience' is used to ensure you are being authenticated for usage on the correct project.

// api audience as defined in auth0

See:

var audience = 'https://your-php-crud-api/api.php'; // api audience as defined in auth0

And you don't have to implement the audience check, it should be part of the configuration:

"jwtAuth.audiences": The audiences that are allowed, empty means 'all' ("")

See:

'aud' => $this->getArrayProperty('audiences', ''),

It seems your questions are mainly about how OAuth with JWT tokens work and not about the PHP-CRUD-API open source project. Please read the PHP-CRUD-API readme and check out the implementation examples and ask questions relating to the documentation and those examples instead. It would be much more useful to others and serves the same purpose.

The firebase example uses a Javascript library (firebasejs v6.0.2) to operate, see: https://github.com/mevdschee/php-crud-api/blob/main/examples/clients/firebase/vanilla.html#L21

Questions about the functioning of firebasejs (f.i. how it sets the audience) should be asked here: https://github.com/firebase/firebase-js-sdk

NB: Friendly reminder that this is not a Firebase support forum and on a more personal note: I don't recommend using Firebase as there are many more privacy-friendly alternatives.

@mevdschee Thank you for your response. My main doubt was in verifying the authenticity of the JWT, but I hadn't seen the possibility of indicating the audiences in the config. I apologize for the conversation veering off the main focus. We can consider this closed, as I now understand how to proceed. Thank you again for your work, and thanks to @nik2208 for his help and patience. Greetings to both of you!

but I was concerned about the possibility that someone could enter my API using another Firebase project to obtain a valid 'Firebase-signed' jwt token.

@kildos I re-read what you wrote and can only agree with @nik2208 that authentication is not authorization and both topics shouldn't be confused and/or merged. If Firebase says somebody has a specific email address and that claims comes verifiable from Firebase, then I would say the authentication process is done (and you should trust that to be true). Realize that whether or not the user with that email address has permissions in your API shouldn't depend on some projectid or audience. Nevertheless I explained how the audience can be checked, but this is mainly to prevent a stolen jwt to be used on other projects (reduce the impact of a leaked token), not for authorization.

To summarize: The authentication middleware (Firebase) tells you who the user is and the authorization middleware decides whether or not that user has access to your API (how is up to you).

@mevdschee
Yes, I understand that completely. My words may have not been the right ones, or I may have skipped a part of the equation, which led you to think that I was including authentication and authorization concepts together. But that's not the case.

Once authenticated, I have implemented ways to determine what the authenticated user can access or not. However, what I was referring to was the concern that api.php (which acts as the gatekeeper for every remote request) could receive fraudulent authentication. After seeing that I can add 'audiences' and 'issuers' filters, my doubt (and my concern) is resolved.

I hope I have been understood.

After seeing that I can add 'audiences' and 'issuers' filters, my doubt (and my concern) is resolved.

Yes, those filters can help to reduce the impact of stolen tokens (from other Firebase applications or clients).

You seem to have thorough understanding of this difficult subject. I think you have and are understood :-)

If someone knew that steve.jobs@apple.com was registered in my database, that person could create a project in Firebase, create a fake user with that email, and obtain a valid token. Then, if that person sent a request to my API with that token, my API would let them in and believe it's the real steve.jobs@apple.com, because the 'claims' would contain that email. Am I wrong?

Yes, I think you might be wrong (edit: you seem to be right). You are saying that Firebase wouldn't verify the identity when creating the token for steve.jobs@apple.com, while that is exactly what an authentication service is supposed to do. Not that I have done any (security) evaluation on Firebase (edit: now I've done a little), but it would surprise me if it was so lax about that (edit: I am surprised). I think it will verify access to the email address before creating the user account and thus the token (edit: it seems it doesn't).

I watched this and this and I hope it is not how Firebase works as passwords are not hashed nor are email addresses verified. I already wasn't a fan of Firebase, but honestly this is kind of shocking (if I understand it correctly). I really can't recommend using Firebase as authentication provider now that I have looked (very shallow) into how it works.

@mevdschee
On one hand, I'm glad I was not crazy in my reasoning. When I saw that Firebase allows user creation and logging without the need for email verification, I realized that what I was commenting in this issue could be a real threat.

I think it would be nice to include in the API documentation that, in the case of Firebase, it's important (or even mandatory) to check other parameters such as the issuer or the audience.

I believe in this case, perhaps I wasn't understood earlier because you assumed Firebase's authentication system was more robust than it actually is, but as long as Firebase does not require the email verification for their users creation and login, someone can create legitimate jwt codes, signed by Google, that authenticates an attacker with any email he wants.

In my case, I chose Firebase for the simplicity of the SDK for iOS and the easy-to-implement user flow to recover their passwords, although I may need to reconsider it.

Thank you for keeping follow on the issue and your research @mevdschee ❤️

I wasn't understood earlier because you assumed Firebase's authentication system was more robust than it actually is,

Yes, that's true. And it seems that Auth0 has a similar "problem", so it is not even Firebase specific.

I think it would be nice to include in the API documentation that, in the case of Firebase, it's important (or even mandatory) to check other parameters such as the issuer or the audience.

I agree, at least the audience requirement should be set for proper security, I'll add that to the readme. How would issuer help?

Thank you for keeping follow on the issue and your research @mevdschee ❤️

Thank you for doing the same.

@kildos Do approve of the additions to the readme? Is it clear enough?

I've been making improvements to PHP-API-AUTH maybe at this point it could be valuable to invest some time on it. What I want to achieve is a self-hosted identity provider acting as hub for other identity providers (firebase, auth0, facebook, whatever) merging the incoming user and the local user before reaching the actual app.

Still missing a key refresh/rotation, reasoning abt whats the best solution. but it works quite well. it needs proper configuration (i mantained the structure @mevdschee gave to the project) but is very flexible and handy once u find yourself around (I can easily define jwt access also on development environment).

As soon as I'll find time to abstract my implementation I'll make a PR if u think it can be useful.

I agree, at least the audience requirement should be set for proper security, I'll add that to the readme. How would issuer help?

Well, in fact, it's practically the same as the audience, but adding Google's base URL. Since the project ID is unique, if the audience is configured, it's enough.

@kildos Do approve of the additions to the readme? Is it clear enough?

Yeah, that's great. Very clear, I'm sure it will be helpful for everyone.

Thank you for your listen and to @nik2208 for what he's doing. If there's a PR soon, please mention me so I can implement any new behaviour on this ❤️

@kildos here's the PR