I've successfully used cookies before and I'd like to begin using JWT. My biggest question is how to pass your token to a website during the initial GET operation, for example when a user types your domain into their address bar or clicks on a link from some other website like google.
When using cookies for example, if you type stackoverflow.com into your web browser, the persistent cookie is sent to the website which seamlessly allows your own stackoverflow session to be automatically authorized.
I am aware that I can programatically pass my JWT token via a javascript GET through the HTTP headers but how do you pass the token when a visitor types in your URL into their web browser?
Possible solution #1
My thoughts have been to have javascript code check if 'authorized'. If not, check for a JWT token in local storage. If found, redirect to the same address. The problem of course would be that there is no way to pass the token during a redirect.
Possible solution #2
Similar to above but not issuing a redirect, I would update the current page to reflect the 'authorized' state.
Possible solution #3
Use a permanent cookie containing the JWT token. I am thinking that this 3rd option would be the best. If I did this, there would be no need to pass the JWT via an HTTP header.
I've thought about this for a few days, read up on JWT and here are my conclusions for avoiding JWT in my particular case:
No easy way to authorize a user who opens their browser and types in your website. With cookies, your server immediately knows how to respond to this headerless GET request.
No way to easily change the JWT token signature. All users are immediately affected by such a change, essentially forcing everyone to authenticate again.
No way to easily invalidate a specific JWT token. The best you can do is to maintain and check a banned signature list on the server. This of course would require a centralized or distributed server method almost identical to a cookie session management system. This would force a coupling between the token and the server, no longer stateless as intended by JWT.
SUMMARY
Cookie management requires more server infrastructure but you have much greater session control. State is seamless (in the case of #1 above). You can instantly invalidate state. For example, the user can log out (deleting the session at the server level) or the session can be instantly banned by an administrator by simply deleting the session.
I also see the benefits to JWS:
no need to hit the db or cache system when authorizing.
simple authorization between multiple servers having the secret key.
simple authorization, no session management programming and no db session state storage required.
...but the drawbacks stated previously are too great for my particular needs.
Related
I'm looking for a way to restrict user access to specific clients in a realm.
I know I can do it with client where Authorization is enabled (fine-grained authorization support) but it doesn't work when trying to connect from front (client need to be public and not confidential).
I'm using a javascript application to login from front-end.
Is there a way to enable Authorization for public client or a work around ?
Thanks.
I'm not sure if this will totally answer your question because it's still not specific enougth but it may give you some further help.
In case you're new to the topic, please see difference between public and confidential clients here.
The current best practice for public clients like HTML/Javascipt applications is to use OpenId Connect with the Authorization Code Flow + PKCE. HTTPS is of course a must have. I recommend you use a javascript openid connect adapter for this like the following for example:
https://github.com/panva/node-openid-client
Basically your authentication / authorization flow is shown here:
When the user wants to login from your frontend client application first a unique verifier is generated which is only available to the exact user / browser session. This value get's hashed as a code challege. Then the user gets redirected to the login page of your authorization server (Keycloak for example) passing some parameters like a redirect uri and the challenge.
With successful login the user get's a session at the keycloak server which also stores the hashed challenge. Then the user gets redirected to given redirect uri (a path in your application) together with a code to obtain an access token. Back in your application you application uses the original value together with the code to get the actual token. The authorization server ckecks the value against the stored challenge and geturns the access token if it matches. You see the extra verifier is to prevent that anybody compromises your code fragment to obtain a token on your behalf.
Now you have an encoded access token in your browser app. Note the token itself is normally only encoded not encrypted but it can be signed. Those signatures can later be used from your backend to ckeck the token integrity but we will come to that soon. Roles, claimes, scopes and so on included in your access token tell you the privileges of the user/identity. You can of course use them to enable/disable functions in your app, block routes etc. but in the end client protection is never really effective so your real authorization ande resource protection happens at your resource server which is your backend (node, .net core, java etc.) maybe a restful Web Api. You pass your access token as a part of the http request header with every request to the backend. Now your backend checks the token integrity (optional) expiration time etc. analyzes scopes, claimes and roles to restrict the resource access.
For example a simple GET myapi/car/{1} may only need a token or can even be annonymous while a POST myapi/cars or PUT myapi/car/{1} may need a special role or higher privileges.
Does that help you out?
Somehow implementing stateless authentication always brings me headaches.
This time it concerns silent auth vs refresh tokens.
Using refresh tokens seems discouraged, however there are certain arguments I don't really get.
If you use an http only cookie to store your refresh token, what exactly is the danger?
Attackers cannot get access the cookie with Javascript and if you use SSL (which you should), I really don't understand the problem.
The resources I read always say "you should not store sensitive data in the client". Seems like an automatic, but that is implicitly impossible if you want to eliminate the need for server session state. Neither do I really understand why, since no resource ever explains how it would be cracked (and I really wonder if anybody really knows).
The reason why I have this question is because using a refresh token offers me more than just authentication.
If a user for example loses his / her device, removing the refresh token will just invalidate all access tokens over all devices (not only browser), which seems like something a user wants to do.
After all, it makes sense that when you lose a device, you need to take action to protect your data.
So the argument "if the attacker gets access to the refresh token, he can infinitely refresh your token" sounds like another argument I don't get. The attacker should not get the refresh token. How would he ever get it? It's the same as saying "if the attacker gets hold of the code of your bank card, he has infinite access to you money". Well if you lose your bank card, you call card stop; likewise if you lose your refresh token, you would delete it to invalidate all access tokens. So how is this an argument?
Can you clarify why I cannot just store my refresh token in an http-only cookie, and how a silent authentication flow improves on this?
Edit:
Note that I read a few other articles that advise to store jwt in the browser by sending the encrypted jwt signature in an http-only cookie. These articles received a lot of upvotes, so that is suddenly okay. It makes zero sense to me.
Edit on comment:
The architecture is very simple:
React / Redux SPA with REST api in the backend
Need for social login through Google, LinkedIn, Github
Need to refresh the token without needed user interaction
Access my own api resources (preferably with jwt)
Ability to revoke refresh token
I don't know why it seems complex (lol).
Refresh tokens are widely used in:
Server side web apps, where they are stored in an HTTP only cookie, as you suggest
Desktop and mobile apps, where they can also be stored in OS secure storage
Refresh tokens should not be infinitely renewable and often represent the user session time - eg:
Refresh token / User session lifetime = 12 hours
Access token / API message credential lifetime = 60 mins
The concern for SPAs in the above article is that there is no real secure storage in the browser - though you are not intending to use browser storage - so no problems there.
One risk is that users can maybe get the secure cookie and replay it to an API via browser developer tools:
To mitigate this it is of course important to ensure that APIs have well engineered authorization - and that what the user can do with a token matches what they can do in the UI.
Another risk is CSRF where a malicious app in another browser tab sends the same cookie to your back end. So you'll need to protect against this.
Note that SPAs have their own token renewal solution based on Authorization Server cookies - I would prefer that option if using an SPA, rather than issuing your own cookie.
I am developing a REST application with its own authentication and authorization mechanism. I want to use JSON Web Tokens for authentication. Is the following a valid and safe implementation?
A REST API will be developed to accept username and password and do the authentication. The HTTP method to be used is POST so that there is no caching. Also, there will be SSL for security at the time of transit
At the time of authentication, two JWTs will be created - access token and refresh token. Refresh token will have longer validity. Both the tokens will be written in cookies, so that they are sent in every subsequent requests
On every REST API call, the tokens will be retrieved from the HTTP header. If the access token is not expired, check the privileges of the user and allow access accordingly. If the access token is expired but the refresh token is valid, recreate new access token and refresh token with new expiry dates (do all necessary checks to ensure that the user rights to authenticate are not revoked) and sent back through Cookies
Provide a logout REST API that will reset the cookie and hence subsequent API calls will be rejected until login is done.
My understanding of refresh token here is:
Due to the presence of refresh token, we can keep shorter validity period for access token and check frequently (at the expiry of access token) that the user is still authorized to login.
Please correct me if I am wrong.
A REST API will be developed to accept username and password and do
the authentication. The HTTP method to be used is POST so that there
is no caching. Also, there will be SSL for security at the time of
transit
This is the way most do it, so you're good here.
At the time of authentication, two JWTs will be created - access token
and refresh token. Refresh token will have longer validity. Both the
tokens will be written in cookies so that they are sent in every
subsequent requests
Storing the tokens in cookies I not dangerous in itself, but if you somehow get you JWT module on your server to read them from there you vulnerable to CSRF attacks where any webpage can trigger a users browser to send a form + you sites cookie to your server unless you use CSRF tokens. So generally they are stored in localStorage and "manually" added to request headers every time.
On every REST API call, the tokens will be retrieved from the HTTP
header. If the access token is not expired, check the privileges of
the user and allow access accordingly. If the access token is expired
but the refresh token is valid, recreate new access token and refresh
token with new expiry dates (do all necessary checks to ensure that
the user rights to authenticate are not revoked) and sent back through
Cookies
Apart from the cookie dangers, it seems safe.
Provide a logout REST API that will reset the cookie and hence
subsequent API calls will be rejected until login is done.
You don't even need to make an API call, you can simply just purge the cookies or the localStorage object and make sure your client doesn't break on missing tokens.
The standard for the express-jwt module expects the tokens to be in its own "Authorization: Bearer [Token]" header, which I would strongly recommend over cookies. The localStorage API is available all the way back to IE8 so you should be good.
Edit:
First, it's important to know the difference between XSS and CSRF attacks since they're often believed to be the same thing.
XSS is when users get unsafe JS running on your domain in other users browsers when that happens neither JWT in localStorage or sessions and JWT in cookies are safe. With httpOnly flag on cookies, you can't directly access them, but the browser will still send them with AJAX requests to your server. If this happens you generally out of luck. To prevent this, make sure to escape all user input if it's sent to the browser.
If you load 3rd party JS with script tags or iframes this might compromise localStorage unless you are careful, but I haven't worked enough with this to help you here.
CSRF is only when other domains are trying to send normal HTML forms to your server by getting the browser to send cookies automatically. Frameworks prevent this by inserting unique random strings as hidden fields and checking them again when it's submitted. JWT's in localStorage is safe from this since each domain gets its own separate localStorage area.
But ultimately all this depends on if your service will be using one single domain, in which case httpOnly cookies will be plenty secure and easier to set up, but if you wanna spread your service out on multiple domains like api.domain.com + app.domain.com or add a native app you're forced to store you're JWTs in localStorage or some other native storage area.
Hope this helps!
I asked this question two years back and also accepted the answer. However, based on my experience and study in the last two years, I'd like to answer this just in case someone stumbles on this thread with the same question.
The approach mentioned in the question is similar to the "Resource Owner Password Credentials" grant type of OAuth 2.0. However, I think it is better to use the "Authorization Code Grant" type instead and Cookie to store the tokens instead of browser localStorage or sessionStorage. I have detailed my reasons, implementation points, security considerations and references in this StackOverlow answer.
Like OP I been using resource owner password grant.
I learned so much from Saptarshi Basu's other answer in a different post I think anyone looking into OAuth Code Flow should take a look at it, it has outlined a very solid approach to auth SPA and resource servers. It primarily relies on your backend(resource server) to handle authentication with the auth provider as a private client.
However, I will just add that people looking at implementing authentication with SPA should also consider OAuth Code Flow with PKCE. The main goal of PKCE is to allow public client such as SPA to authenticate directly with auth provider. All PKCE adds, is that when a SPA app initiates authentication, a hashed value is sent to the auth provider when the user is authenticated. And after user authenticate with the authorization provider, it redirects the user back to SPA with that hashed value as well as authorization code. Now, for the next part where the SPA calls auth provider to exchange code for tokens, instead of providing client secret, it has to provide the key that was originally used to create the hashed value. This mechanism guarantees the code cannot be used by someone who intercepted the code, and the SPA doesnt need to store a client secret like a server-side app does.
Now the only thing I'm not certain at this point is which is technically more secure, server-side authentication using standard Code Flow without PKCE or SPA authenticating directly using PKCE? Most resources I could find online currently describes and recommends the latter . However I feel that letting a private server side client handle authentication (as Saptarshi Basu described) might still be more secure. I would love to hear his opinion on this as well.
My understanding of refresh token here is:
Due to the presence of refresh token, we can keep shorter validity period for access token and check frequently (at the expiry of access token) that the user is still authorized to login.
Please correct me if I am wrong.
Assuming you're talking about using JWT as Bearer-token in OAuth (and I would strongly advice to follow the OAuth 2.0 protocol), that's right.
With an additional auth-time (timestamp of authentication) claim in your JWT, you could even drop the second token and sent your access- as a refresh-token (the auth-server could then issue a new access-token if token is valid & auth-time within allowed range)... but sure, it's also good to follow the standard ;)
Anyway, there are certain additional aspects (that tend to get difficult or are even against the fundamental ideas of JWT) you should consider before using JWTs as refresh-token, as this basically means you introduce long-living JWT:
do you need to have something like forced user logout/ token revocation by subject (e.g. if user got identified as fraudulent)?
do you need to have something like revocation of a specific token (e.g. if a user looses a device)?
...
Dependent on your use-case you should consider all the possible implications, long-living tokens have as they usually require you to introduce some kind of state on your server-side (e.g. to allow revocation/ blacklisting). Keep in mind the beauty and security of the JWT concept lies within JWTs being short-lived.
TL;DR When using google oauth on desktop app, what to save on disk to avoid repeated sign in? Save the google user id? or the token? or an session id?
I'm creating an little desktop app, whitch must authenticate to my REST API server. I'm using google oauth2 for that.
The idea is, that when the desktop app will be authentivated, it generates some data that will be send to my server. The server will store the data with the google user id received from https://www.googleapis.com/userinfo/v2/me.
On the first run of the desktop app, it will open the default browser, with and url for my server and start an local http server. then:
my server will redirect the browser to google (with the clientid, secret, etc.)
user logs in and it will be redirected back to the server with the oauth code
server uses the code to get the token, and then the user profile and stores the token and the profile in db, then redirects the browser to localhost with an paramerer
the desktop app catches the parameter and stores it in an file on the disk
next time the desktop app will start it only reads the file for the parameter to send the generated data with it to my server
my question is: what the parameter should be? the google user id? the oauth token? an generated session id for this desktop app? or something else?
when it will be the google user id, it can conveniently sent the data with the user id and the rest server will just store it in db as is. but I don't think it's safe
when it will be the token, the rest server has to with every request also get the user profile from google with the token. and imho sending the token with every request isn't safe either
generating an session id means to store it with the user and the token on the server and the desktop app will just store it and send it with every request. but I don't know if it's safe to do that
As it's normally the case in software development you have a couple of options depending on requirements.
The mandatory requirement is that your client (desktop) application needs to send something to your REST API so that the API can perform up to two decisions:
Decide who the user is.
Decide if the user is authorized to perform the currently requested action.
The second step may not be applicable if all authenticated users have access to exactly the same set of actions so I'll cover both scenarios.
Also note that, for the first step, sending the Google user ID is not a valid option as that information can be obtained by other parties and does not ensure that the user did authenticate to use your application.
Option 1 - Authentication without fine-grained authorization
Either always sending the id_token or exchanging that token with your custom session identifier both meet the previous requirement, because the id_token contains an audience that clearly indicates the user authenticated to use your application and the session identifier is generated by your application so it can also ensure that. The requests to your API need to use HTTPS, otherwise it will be too easy for the token or session ID to be captured by an attacker.
If you go with the id_token alternative you need to take in consideration that the token will expire; for this, a few options again:
repeat the authentication process another time; if the user still has a session it will indeed be quicker, but you still have to open a browser, local server and repeat the whole steps.
request offline_access when doing the first authentication.
With the last option you should get a refresh token that would allow for your application to have a way to identify the user even after the first id_token expires. I say should, because Google seems to do things a bit different than the specification, for example, the way to obtain the refresh token is by providing access_type=offline instead of the offline_access from OpenID Connect.
Personally, I would go with the session identifier as you'll have more control over lifetime and it may also be simpler.
Option 2 - Authentication + fine-grained authorization
If you need a fine-grained authorization system for your REST API then the best approach would be to authenticate your users with Google, but then have an OAuth 2.0 compliant authorization server that would issue access tokens specific for your API.
For the authorization server implementation, you could either:
Implement it yourself or leverage open source components
⤷ may be time consuming, complex and mitigation of security risks would all fall on you
Use a third-party OAuth 2.0 as a servive authorization provider like Auth0
⤷ easy to get started, depending on amount of usage (the free plan on Auth0 goes up to 7000 users) it will cost you money instead of time
Disclosure: I work at Auth0.
There should be no problem sending the access_token with every request since they are created for that purpose and are thus short lived. You can use the Google Authorization Server endpoint to verify a token instead of using it to do a request for a users profile.
If you're only relying on Google for authentication, here's how your workflow can look:
the client (desktop application, in your case) retrieves the
Google id_token following the user's log in, and then sends it to
the server
the server validates the integrity of said token and extracts the user's profile data; this could mean a simple GET on Google's endpoint to verify this token: https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}
On subsequent requests, nothing should change really, except that the user's login process will be automated (since he's given permissions & all), and thus much faster. #danielx is right, there's no problem with sending the token each and every time.
Preface
I'm developing several web services and a handful of clients (web app, mobile, etc.) which will interface with said services over HTTP(s). My current work item is to design an authentication and authorization solution for the product. I have decided to leverage external identity providers, such as Facebook, Google, Microsoft, Twitter, and the like for authentication.
I'm trying to solve the problem of, "when a request comes to my server, how do I know who the user is and how can I be sure?". More questions below as well...
Requirements
Rely on external identities to indicate who I'm dealing with ('userId' essentially is all I care about).
The system should use token-based authentication (as opposed to cookies for example or basic auth).
I believe this is the right choice for scaling across multiple clients and servers while providing loose coupling.
Workflow
Based on my reading and understanding of token-based authentication, the following is how I imagine the workflow to be. Let's focus for now on Facebook in a web browser. My assumption is that other external identity providers should have similar capabilities, though I have not confirmed just yet.
Note, as of writing, I'm basing the following off of Facebook login version 2.2
Client: Initiates login to Facebook using the JavaScript SDK
Facebook: User authenticates and approves app permissions (to access user's public profile for example)
Facebook: Sends response to client which contains user’s access token, ID, and signed request
Client: Stores user access token in browser session (handled by SDK conveniently)
Client: Makes a request to my web service for a secure resource by sending along the user’s access token in the authorization header + the user’s ID (in custom header potentially)
Server: Reads user access token from request header and initiates verification by sending a request to the debug_token graph API provided by Facebook
Facebook: Responds back to the server with the user access token info (contains appId and userId)
Server: Completes verification of the token by comparing the appId to what is expected (known to itself) and the userId to what was sent on the client request
Server: Responds to the client with the requested resource (assuming the happy authorization path)
I’m imagining steps 5-9 would be repeated for subsequent requests to the server (while the user’s access token is valid – not expired, revoked from FB side, app permissions changed, etc.)
Here's a diagram to help go along with the steps. Please understand this system is not a single page application (SPA). The web services mentioned are API endpoints serving JSON data back to clients essentially; they are not serving HTML/JS/CSS (with the exception of the web client servers).
Questions
First and foremost, are there any glaring gaps / pit falls with the described approach based on my preface and requirements?
Is performing an outbound request to Facebook for verifying the access token (steps 6-8 above) per client request required / recommended?
I know at the very least, I must verify the access token coming from the client request. However, the recommended approach for subsequent verifications after the first is unknown to me. If there are typical patterns, I’m interested in hearing about them. I understand they may be application dependent based on my requirements; however, I just don’t know what to look for yet. I’ll put in the due diligence once I have a basic idea.
For instance, possible thoughts:
Hash the access token + userId pair after first verification is complete and store it in a distributed cache (accessible by all web servers) with expiry equal to access tokens. Upon subsequent requests from the clients, hash the access token + userId pair and check its existence in the cache. If present, then request is authorized. Otherwise, reach out to Facebook graph API to confirm the access token. I’m assuming this strategy might be feasible if I’m using HTTPS (which I will be). However, how does performance compare?
The accepted answer in this StackOverflow question recommends creating a custom access token after the first verification of the Facebook user token is complete. The custom token would then be sent to the client for subsequent requests. I’m wondering if this is more complex than the above solution, however. This would require implementing my own Identity Provider (something I want to avoid because I want to use external identity providers in the first place…). Is there any merit to this suggestion?
Is the signedRequest field present on the response in step #3 above (mentioned here), equivalent to the signed request parameter here in the ‘games canvas login’ flow?
They seem to be hinted as equivalent since the former links to the latter in the documentation. However, I’m surprised the verification strategy mentioned on the games page isn’t mentioned in the ‘manually building a login flow’ page of the web documentation.
If the answer to #3 is ‘Yes’, can the same identity confirmation strategy of decoding the signature and comparing to what is expected to be used on the server-side?
I’m wondering if this can be leveraged instead of making an outbound call to the debug_token graph API (step #6 above) to confirm the access token as recommended here:
Of course, in order to make the comparison on the server-side, the signed request portion would need to be sent along with the request to the server (step #5 above). In addition to feasibility without sacrificing security, I’m wondering how the performance would compare to making the outbound call.
While I’m at it, in what scenario / for what purpose, would you persist a user's access token to a database for example?
I don’t see a scenario where I would need to do this, however, I may be overlooking something. I’m curious was some common scenarios might be to spark some thoughts.
Thanks!
From what you describe I'd suggest to use a server-side login flow as described in
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2
so that the token is already on your server, and doesn't need to be passed from the client. If you're using non-encrypted connections, this could be a security risk (e.g. for man-in-the-middle attacks).
The steps would be:
(1) Logging people in
You need to specify the permission you want to gather from the users in the scope parameter. The request can be triggered just via a normal link:
GET https://www.facebook.com/dialog/oauth?
client_id={app-id}
&redirect_uri={redirect-uri}
&response_type=code
&scope={permission_list}
See
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2#login
(2) Confirm the identitity
GET https://graph.facebook.com/oauth/access_token?
client_id={app-id}
&redirect_uri={redirect-uri}
&client_secret={app-secret}
&code={code-parameter}
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2#confirm
(3) Inspect the access token
You can inspect the token as you already said in your question via
GET /debug_token?input_token={token-to-inspect}
&access_token={app-token-or-admin-token}
This should only be done server-side, because otherwise you'd make you app access token visible to end users (not a good idea!).
See
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2#checktoken
(4) Extending the access token
Once you got the (short-lived) token, you can do a call to extend the token as described in
https://developers.facebook.com/docs/facebook-login/access-tokens#extending
like the following:
GET /oauth/access_token?grant_type=fb_exchange_token
&client_id={app-id}
&client_secret={app-secret}
&fb_exchange_token={short-lived-token}
(5) Storing of access tokens
Concerning the storing of the tokens on the server, FB suggests to do so:
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2#token
(6) Handling expired access tokens
As FB doesn't notify you if a token has expired (and if you don't save the expiry date and compare this to the current timestamp before making a call), it's possible that you receive error messages from FB if the token got invalid (after max. 60 days). The error code will be 190:
{
"error": {
"message": "Error validating access token: Session has expired at unix
time SOME_TIME. The current unix time is SOME_TIME.",
"type": "OAuthException",
"code": 190
}
}
See
https://developers.facebook.com/docs/facebook-login/access-tokens#expiredtokens
If the access token becomes invalid, the solution is to have the person log in again, at which point you will be able to make API calls on their behalf once more. The login flow your app uses for new people should determine which method you need to adopt.
I dont' see any glaring gaps / pit falls, but I'm not a security expert.
Once your server has verified the given token (step 8), as you said:
The accepted answer in this StackOverflow question recommends creating a custom access token after the first verification of the Facebook user token is complete. The custom token would then be sent to the client for subsequent requests. I’m wondering if this is more complex than the above solution, however. This would require implementing my own Identity Provider (something I want to avoid because I want to use external identity providers in the first place…). Is there any merit to this suggestion?
IMHO is the way to go. I would use https://jwt.io/ which allows you to encode values (the userId for example) using a secret key.
Then your client attach this token to every request. So you can verify the request without need to a third party (you don't need database queries neither). The nice thing here is there is no need to store the token on your DB.
You can define an expiration date on the token, to force the client authenticate with the third party again when you want.
Let's say you want your server be able to do some action without the client interaction. For example: Open graph stories. In this scenario because you need to publish something in the name of the user you would need the access token stored on your DB.
(I can not help with the 3 and 4 questions, sorry).
Problem with Facebook is that they do not use OpenId connect on top of Oauth (https://blog.runscope.com/posts/understanding-oauth-2-and-openid-connect).
Thus resulting in their custom ways of providing Oauth authentification.
Oauth2 with OpenId connect identity services usually provide issuer endpoint where you can find URL (by appending ".well-known/openid-configuration") for jwk's which can be used to verify that JWT token and its contents were signed by the same identity service. (i.e access token originated from the same service that provided you jwk's)
For example some known openid connect identity providers:
https://accounts.google.com/.well-known/openid-configuration
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
(btw it is not a coincidence that Attlasian provides only these two services to perform external login)
Now as you mentioned, you need to support multiple oauth providers and since like Facebook not all providers use same configuration of oauth (they use different JWT attribute names, toke verification methods, etc. (Openid connect tries to unify this process)) i would suggest you to use some middleware identity provider like Oauth0 (service not protocol) or Keycloak. These can be used with external identity providers (Social pages as you mentioned) and also provides you with custom user store.
Advantage is that they unify authentication process under one type (e.g both support openid connect). Whereas when using multiple oauth providers with not unified authentication workflow you will end up with redudant implementations and need for merging different information's under one type (this is basically what mentioned middle-ware identity providers solve for you).
So if you will use only Facebook as identity provider in your app then go for it and make implementation directly for Facebook Oauth workflow. But with multiple identity providers (which is almost always case when creating public services) you should stick with mentioned workaround or find another one (or maybe wait till all social services will support Openid connect, which they probably wont).
There may be hope.. This year, Facebook have announced a "limited login" feature, which, if they were to add to their javascript sdks would certainly make my life easier:
https://developers.facebook.com/blog/post/2021/04/12/announcing-expanded-functionality-limited-login/
At the time of writing, I can only find reference to iOS and Unity SDKs, but it does seem to return a normal JWT, which is all I want!