pfy.ch

I had to do this over a span of a few days and found the documentation completely lacking, especially since I was not using AWS Amplify to create my app.

My use cases for this implementation are:

My restrictions imposed are:

If you didn’t already know, it’s possible to use Amazons Amplify components without having your project be built with Amplify. This greatly decreases the amount of code required for setting up things like Auth, however your on your own when it comes to stuff like this. Here is what our existing Authentication setup in React looked like:

Amplify.configure({
  Auth: {
    // Auth setup goes here, removed to shorten.
  },
});

const App = () => {
  return (
    <div className="app">
      <Authenticator>
        <h1>Authed!</h1> 
      </Authenticator>
    </div>
  );
}

This works for standard cognito Authentication. However our new use cases requires forcing users to Authenticate with Facebook.

We use CloudFormation for all of our infrastructure so I’ll be showing the resource changes we made to allow facebook authentication. I am assuming you already have the following configured as these are what we will be changing:

The Facebook App

The first step was to create a Facebook App. This was relatively standard however since we required access to the Groups API we had to create a “Business” app. I found the permissions’ system in facebook a bit frustrating as without having read all the docs first, it’s not easy to know what kind of app you’ll need. Once this app is configured you should have an App ID and an App Secret.

I’ll not be going into getting your App Approved by Facebook.

Cloud Formation

The first step is to add a AWS::Cognito::UserPoolIdentityProvider to your CloudFormation.

UserPoolIdentityProvider:
  Type: AWS::Cognito::UserPoolIdentityProvider
  Properties:
    ProviderName: Facebook
    AttributeMapping:
      name: name
      email: email
      "custom:fb_access_token": access_token
    ProviderDetails:
      client_id: "Facebook App ID" 
      client_secret: "Facebook App Secret" 
      authorize_scopes: "public_profile,groups_access_member_info" 
    ProviderType: Facebook
    UserPoolId:
      Ref: CognitoUserPool # Reference your user pool

You may notice "custom:fb_access_token": access_token under AttributeMapping. This is required since cognito does not store the users Facebook access token anywhere accessible by default. This had me stumped for the longest time and is what pushed me to write this guide!

You’ll extend your AWS::Cognito::UserPool definition with the following under properties to store this value:

Schema:
  - AttributeDataType: String
    Name: fb_access_token
    Mutable: true

This adds the "custom:fb_access_token" property to your pool. You can name this whatever you want but this made the most sense to me at the time.

The next thing we needed to extend was our AWS::Cognito::UserPoolClient. Under it’s Properties we added the following:

AllowedOAuthFlowsUserPoolClient: true
SupportedIdentityProviders:
  - Facebook
CallbackURLs:
  - http://localhost:3000 # Remove this for prod or use an env var
  - https://your-live-url.com
LogoutURLs:
  - http://localhost:3000 # Remove this for prod or use an env var
  - https://your-live-url.com
AllowedOAuthFlows:
  - code
AllowedOAuthScopes:
  - email
  - openid
  - aws.cognito.signin.user.admin
  - profile
  - phone

This sets up your Cognito User Pool to work with Facebook Federated Identities. The final change required to the CloudFormation is a Domain for Cognito to use for OAuth.

CognitoUserPoolDomain:
  Type: AWS::Cognito::UserPoolDomain
  Properties:
    Domain: my-oauth-domain 
    UserPoolId: 
      Ref: CognitoUserPool # Reference your user pool

Domain is short since Cognito will automatically tack on the extra link cruft! The final domain will look something like my-oauth-domain.auth.ap-southeast-2.amazoncognito.com.

I recommend exporting this domain as well as the CallbackURLs and LogoutURLs from your AWS::Cognito::UserPoolClient as they are Required in your React App. If your URLs are driven by env vars ensure your React App has access to them.

React App

Once the cloud formation is set up, you can update your <Authenticator /> with the following:

<Authenticator socialProviders={['facebook']} hideSignUp>
  <h1>Authed!</h1> 
</Authenticator>

We add hideSignUp since we only want users to sign in with Facebook, this is not required.

Next, inside your Amplify Configuration, you need to add the oauth object:

Amplify.configure({
  Auth: {
    // Extra values removed for example... 
    oauth: { // We're adding this!
      domain: "<Domain created with AWS::Cognito::UserPoolDomain (Remove https://)>",
      scope: [
        'phone',
        'email',
        'profile',
        'openid',
        'aws.cognito.signin.user.admin',
      ],
      redirectSignIn: "<A domain included in your CallbackURLs>",
      redirectSignOut: "<A domain included in your CallbackURLs>",
      responseType: 'code',
    },
  },
});

Cognito will not work if redirectSignIn and redirectSignOut domains are not in the list of domains provided to CallbackURLs in your AWS::Cognito::UserPoolDomain definition!

If your existing Cognito Authentication worked, there should now be a Sign in with Facebook button in your Component. When this is clicked Facebook will prompt you to sign in and a user will be created in your Cognito user pool.

Making Facebook API requests

Our API is using express and is configured with an Authorizer through the serverless framework.

facebook:
  handler: src/facebook/facebook.handler 
  events:
    - http:
        path: /facebook/{any+}
        method: ANY
        authorizer:
          type: COGNITO_USER_POOLS
          authorizerId:
            Ref: ApiGatewayAuthorizer 

When a user makes an Authenticated request to this endpoint their Cognito JWT token will be accessible in the request headers:

app.get('/facebook/example', [
  async (req: RequestContext, res: Response) => {
    const CognitoJWT = req.headers.authorization
  }
])

using the following function you can decode the JWT token & get the users details, I recommend logging this out briefly to see what values are made available to you!

const payload = JSON.parse(
  Buffer.from(
    CognitoJWT.split('.')[1], 
    'base64'
  ).toString(),
);

Combining this with our express endpoint we can pull the required values needed to make Facebook requests:

app.get('/facebook/example', [
  async (req: RequestContext, res: Response) => {
    const CognitoJWT = req.headers.authorization
    const payload = JSON.parse(
      Buffer.from(
        CognitoJWT.split('.')[1],
        'base64'
      ).toString(),
    );
    
    const url = "https://graph.facebook.com/" 
    const version = "v15.0" 
    const userId = payload['cognito:username'].split("_")[1];
    const accessToken = payload['custom:fb_access_token'];
    
    const fbRequest = await axios.request({
      method: 'GET',
      url: `${url}${version}/${userId}/groups?access_token=${accessToken}`,
    });
    
    console.log("Users groups:", fbRequest.data)
  }
])

This was a nightmare to figure out, Almost all docs regarding the <Authorizer /> component expect you to be using Amplify CLI or the Amplify UI to manage your app. While using the Amplify Components does save on Boilerplate you really are on your own when it comes to figuring out how it works. Sometimes the best solution is to stand a demo app up with Amplify and then copy what it generates into your actual project.

All of the example code here is in Javascript whereas our actual implantation is in Typescript and is much safer than this example code. I do not recommend you copy any of this example code as it’s been stripped and gutted to make it safe to share. However I do hope that it can provide some assistance if anyone does ever need to figure this out themselves.


© 2024 Pfych