Search code examples
reactjsfacebookoauth-2.0jwtopenid-connect

How to get User Access Token claims for Facebook registered app


I am used to working with Azure Active Directory. I know that if I register an app, I can use MSALs library in React (which uses OIDC in the background) to get a JWT Token (I actually get 2, an ID and an Access Token, but that is beyond the scope of this question). I can then make a call from my front end to my back end API, and send that token through as authentication. In the backend, I check the signature, and also check that the app of the token is the app that I registered, and also that the token is generated for use 'X'. So with that single token I can protect my backend API and make sure that the frontend call is legitimately coming from the SPA (unless the token gets stolen), and that the user 'X' is the one whose session is active for the given API call.

Now, for a personal project, I am trying to work with Facebook as an identity provider. I registered my app in facebook, and in my freshly created react project I have:

<React.StrictMode>
    <FacebookLogin
        appId="<App ID Here>"
        autoLoad={false}
        fields="name,email,picture"
        callback={responseFacebook}
    />
    <App />
</React.StrictMode>

and

const responseFacebook = (response) => {
    console.log(response);
}

Now, when I click the button, I see the following in the console:

{
    "name": "<My Name Here>",
    "email": "<My Mail Here>",
    "picture": "<Picture Data Here>",
    "id": "<ID Here>",
    "accessToken": "EAAJlEXBfRWgBAGHTP0ZAgRttMSMZzAxWM4B6aAZBL7FlGSWgZBITj9HXb2mZAVuoyIc1V43StjYwuwcsSTZBEd4aPK6iGwbNGRvN7o5PH3ZAOymGxtb5W8k2BYYdAW5w1frXt8JeEvJI3SCoOsJqMVHm9mo5N7NpSZBKs74wqu8LvgQbqqYfvialeSP5LwtUMMS51pMsN5Kwv7aWPud0",
    "userID": "<User ID Here>",
    "expiresIn": 6488,
    "signedRequest": "<A Bunch of Gibberish>",
    "graphDomain": "facebook",
    "data_access_expiration_time": 1677528712
}

Now, about the access token, that is no JWT and I dont know how to use it for authenticating against my OWN backend API. I know I can use that token to make requests to facebook GraphAPI and get more info about the user (in this case, me). But, when I make a request to my backend, and I send that token, I have no clue how to verify its a legitimate token, meaning:

  • Verify its signed by facebook
  • Verify the APP ID is the one im looking for
  • Verify for which user was the token generated

In good old JWT, I can simply check the claims, but I have no idea what this is and how to prove to a backend server, or any third party for that matter, that the token was generated by 'X' user interacting with 'Y' app registered on facebook


Solution

  • Well, as per @CBroe's suggestion, I used the debug token Graph endpoint to retrieve info about the token (Doc https://developers.facebook.com/docs/graph-api/reference/v15.0/debug_token).

    So in the front end, I get the info shown in the question, and I need to pass that info to a backend API, as well as proof of said info legitimacy. So, in pseudocode:

    // Frontend
    body = {
        "name": "<My Name Here>",
        "email": "<My Mail Here>",
        "picture": "<Picture Data Here>",
        "id": "<ID Here>",
        "accessToken": "EAAJlEXBfRWgBAGHTP0ZAgRttMSMZz...",
        "userID": "<User ID Here>",
        "expiresIn": 6488,
        "signedRequest": "<A Bunch of Gibberish>",
        "graphDomain": "facebook",
        "data_access_expiration_time": 1677528712
    };
    RestClient.call("myapiurl/myendpoint", "HTTP_METHOD", body=body);
    

    and

    // Backend
    function checkValidity(info) {
        version = "v15.0"
        token = info.accessToken;
        endpointURL = "https://graph.facebook.com/${version}/debug_token?input_token=${token}";
        headers = { "Authorization": "Bearer ${token}" };
        response = RestClient.call(endpointURL, "GET", headers=headers);
        // Note that the token is sent twice, once for Auth purposes, and another time as payload for the debug endpoint
        /*
            The response will look like:
            {
                "data": {
                    "app_id": "<APP ID>",
                    "type": "USER",
                    "application": "<APP Name>",
                    "data_access_expires_at": 1677601862,
                    "expires_at": 1669831200,
                    "is_valid": true,
                    "scopes": [
                        "email",
                        "public_profile"
                    ],
                    "user_id": "<USER ID>"
                }
            }
        */
        if (response.data.app_id == "<MY APP ID>" && response.data.user_id == info.userID) {
            return true;
        } else {
            return false;
        }
    }
    
    app.post('/myendpoint', function(req, res) {
        checkValidity(req.body);
        res.send('some result');
    });
    

    This way we can be sure that the token is valid (as if it wasn't, the facebook endpoint would have returned an error. Also, we can confirm the token was generated by userID for appID. In your backend data system you need to use the userID as your main reference, because its the only guaranteed value to have to map that user's activity in that appID (unless the user allows the app to read the email).

    Note that the userID is different for each user-app pair, so if you want data such as the user email, you need to use the token to hit facebook's /user endpoint with that userID to get the info you need.

    You should not trust the email field sent in the body because, since the AppID is public as its dealt with in the frontend, someone could make up a mock frontend and login with their own account (therefore generating a valid token with its matching userID) and forge the rest of the fields in the body.

    This assumes the token has not been stolen (as with any bearer token solution), and that the user is careful and aware of where he/she is signing into, (as a malicious phishing clone frontend could trick the user into SSOing into a fake site).