Facebook OAuth via Cognito without Amplify
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:
- User can sign in with Facebook and are authenticated via cognito
- User can view the groups they are in from our dashboard
My restrictions imposed are:
- Use existing implementation with
<Authenticator>
from the Amplify library - Users must be stored in Cognito
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:
AWS::Cognito::UserPool
AWS::Cognito::UserPoolClient
AWS::Cognito::IdentityPool
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.