Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT with Firebase for secure authentication #991

Closed
kildos opened this issue Oct 12, 2023 · 30 comments
Closed

JWT with Firebase for secure authentication #991

kildos opened this issue Oct 12, 2023 · 30 comments
Assignees

Comments

@kildos
Copy link

kildos commented Oct 12, 2023

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/[email protected]');
$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 ❤️

@mevdschee mevdschee self-assigned this Oct 13, 2023
@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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;
        },
...

@kildos
Copy link
Author

kildos commented Oct 13, 2023

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 [email protected] 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 [email protected], because the 'claims' would contain that email. Am I wrong?

Thanks a lot for your help

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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.

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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!

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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?

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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": "[email protected]",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"[email protected]"
]
},
"sign_in_provider": "password"
}
}

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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.

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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/[email protected]');
$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

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

are u manually fetching the jwt token from firebase?

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

have a read here #926

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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/[email protected]');
$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/[email protected] 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.

@nik2208
Copy link
Contributor

nik2208 commented Oct 13, 2023

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/[email protected]');
$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?

@kildos
Copy link
Author

kildos commented Oct 13, 2023

@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.

@mevdschee
Copy link
Owner

mevdschee commented Oct 14, 2023

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.

@kildos
Copy link
Author

kildos commented Oct 14, 2023

@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!

@kildos kildos closed this as completed Oct 14, 2023
@mevdschee
Copy link
Owner

mevdschee commented Oct 14, 2023

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).

@kildos
Copy link
Author

kildos commented Oct 14, 2023

@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.

@mevdschee
Copy link
Owner

mevdschee commented Oct 14, 2023

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 :-)

@mevdschee
Copy link
Owner

mevdschee commented Oct 14, 2023

If someone knew that [email protected] 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 [email protected], 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 [email protected], 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).

@mevdschee
Copy link
Owner

mevdschee commented Oct 14, 2023

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.

@kildos
Copy link
Author

kildos commented Oct 15, 2023

@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 ❤️

@mevdschee
Copy link
Owner

mevdschee commented Oct 15, 2023

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.

mevdschee added a commit that referenced this issue Oct 15, 2023
mevdschee added a commit that referenced this issue Oct 15, 2023
@mevdschee
Copy link
Owner

mevdschee commented Oct 15, 2023

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

mevdschee added a commit that referenced this issue Oct 15, 2023
@nik2208
Copy link
Contributor

nik2208 commented Oct 15, 2023

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.

@kildos
Copy link
Author

kildos commented Oct 15, 2023

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 ❤️

@nik2208
Copy link
Contributor

nik2208 commented Oct 15, 2023

@kildos here's the PR

@nik2208
Copy link
Contributor

nik2208 commented Feb 7, 2024

After a few months of investigation and studying, I've eventually accepted that reinventing the wheel could not be the right approach. What is more (much more) interesting, is using supertokens beside this api, and I could make it work. The architecture is explained here
https://supertokens.com/docs/thirdpartyemailpassword/other-frameworks
and a simple (very very simple) node server would handle all the auth process.
I will be adding it here as an enhancement proposal to discuss if integrating this solution is something doable (maybe in docker compose with the proper envs set).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants