Created
February 6, 2021 15:00
-
-
Save FezVrasta/57d29cd2bbc4ed80e169780035f748cf to your computer and use it in GitHub Desktop.
Firebase Twitch OAuth Flow (Cloud Functions implementation)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const functions = require('firebase-functions'); | |
const admin = require('firebase-admin'); | |
const cookieParser = require('cookie-parser'); | |
const crypto = require('crypto'); | |
const { AuthorizationCode } = require('simple-oauth2'); | |
const fetch = require('node-fetch'); | |
// Firebase Setup | |
const admin = require('firebase-admin'); | |
// @ts-ignore | |
const serviceAccount = require('./service-account.json'); | |
admin.initializeApp({ | |
credential: admin.credential.cert(serviceAccount), | |
databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`, | |
}); | |
const OAUTH_REDIRECT_URI = `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`;; | |
const OAUTH_SCOPES = 'user:read:email'; | |
/** | |
* Creates a configured simple-oauth2 client for Twitch. | |
*/ | |
function twitchOAuth2Client() { | |
// Twitch OAuth 2 setup | |
// TODO: Configure the `twitch.client_id` and `twitch.client_secret` Google Cloud environment variables. | |
const credentials = { | |
client: { | |
id: functions.config().twitch.client_id, | |
secret: functions.config().twitch.client_secret, | |
}, | |
auth: { | |
tokenHost: 'https://id.twitch.tv', | |
tokenPath: '/oauth2/token', | |
authorizePath: '/oauth2/authorize', | |
}, | |
options: { | |
bodyFormat: 'json', | |
authorizationMethod: 'body', | |
}, | |
}; | |
return new AuthorizationCode(credentials); | |
} | |
/** | |
* Redirects the User to the Twitch authentication consent screen. Also the 'state' cookie is set for later state | |
* verification. | |
*/ | |
exports.redirect = functions.https.onRequest((req, res) => { | |
const authorizationCode = twitchOAuth2Client(); | |
cookieParser()(req, res, () => { | |
const state = req.cookies.__session || crypto.randomBytes(20).toString('hex'); | |
console.log('Setting verification state:', state); | |
res.cookie('__session', state.toString(), { maxAge: 3600000, httpOnly: true }); | |
const redirectUri = authorizationCode.authorizeURL({ | |
redirect_uri: OAUTH_REDIRECT_URI, | |
scope: OAUTH_SCOPES, | |
state: state, | |
}); | |
console.log('Redirecting to:', redirectUri); | |
res.redirect(redirectUri); | |
}); | |
}); | |
/** | |
* Exchanges a given Twitch auth code passed in the 'code' URL query parameter for a Firebase auth token. | |
* The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie. | |
* The Firebase custom auth token, display name, photo URL and Twitch acces token are sent back in a JSONP callback | |
* function with function name defined by the 'callback' query parameter. | |
*/ | |
exports.token = functions.https.onRequest((req, res) => { | |
const authorizationCode = twitchOAuth2Client(); | |
try { | |
cookieParser()(req, res, async () => { | |
try { | |
console.log('Received verification state:', req.cookies.__session); | |
console.log('Received state:', req.query.state); | |
if (!req.cookies.__session) { | |
throw new Error( | |
'State cookie not set or expired. Maybe you took too long to authorize. Please try again.' | |
); | |
} else if (req.cookies.__session !== req.query.state) { | |
throw new Error('State validation failed'); | |
} | |
} catch (error) { | |
return res.jsonp({ error: error.toString() }); | |
} | |
let accessToken; | |
try { | |
console.log('Received auth code:', req.query.code); | |
const options = { | |
client_id: functions.config().twitch.client_id, | |
client_secret: functions.config().twitch.client_secret, | |
code: req.query.code, | |
grant_type: 'authorization_code', | |
redirect_uri: OAUTH_REDIRECT_URI, | |
}; | |
console.log('Asking token with options', JSON.stringify(options)); | |
accessToken = await authorizationCode.getToken(options); | |
console.log('Auth code exchange result received'); | |
const twitchUser = await getTwitchUser(accessToken.toJSON().access_token); | |
// Create a Firebase account and get the Custom Auth Token. | |
const firebaseToken = await createFirebaseAccount(twitchUser); | |
// Serve an HTML page that signs the user in and updates the user profile. | |
return res.jsonp({ token: firebaseToken }); | |
} catch (error) { | |
return res.jsonp({ error: error.toString() }); | |
} | |
}); | |
} catch (error) { | |
return res.jsonp({ error: error.toString() }); | |
} | |
}); | |
/** | |
* Creates a Firebase account with the given user profile and returns a custom auth token allowing | |
* signing-in this account. | |
* | |
* @returns {Promise<string>} The Firebase custom auth token in a promise. | |
*/ | |
async function createFirebaseAccount(twitchUser) { | |
// The UID we'll assign to the user. | |
const uid = `twitch:${twitchUser.id}`; | |
// Save the access token to the Firebase Database. | |
const db = admin.firestore(); | |
const databaseTask = db.collection('users').doc(uid).set(twitchUser); | |
// Create or update the user account. | |
const userCreationTask = admin | |
.auth() | |
.updateUser(uid, { | |
displayName: twitchUser['display_name'], | |
photoURL: twitchUser['profile_image_url'], | |
email: twitchUser['email'], | |
}) | |
.catch((error) => { | |
// If user does not exists we create it. | |
if (error.code === 'auth/user-not-found') { | |
return admin.auth().createUser({ | |
uid: uid, | |
displayName: twitchUser['display_name'], | |
photoURL: twitchUser['profile_image_url'], | |
email: twitchUser['email'], | |
}); | |
} | |
throw error; | |
}); | |
// Wait for all async task to complete then generate and return a custom auth token. | |
await Promise.all([userCreationTask, databaseTask]); | |
// Create a Firebase custom auth token. | |
const token = await admin.auth().createCustomToken(uid); | |
console.log('Created Custom token for UID "', uid, '" Token:', token); | |
return token; | |
} | |
async function getTwitchUser(accessToken) { | |
console.log('Fetching Twitch user with access_token', accessToken); | |
try { | |
const response = await fetch('https://api.twitch.tv/helix/users', { | |
method: 'GET', | |
headers: { | |
'Client-Id': functions.config().twitch.client_id, | |
Authorization: 'Bearer ' + accessToken, | |
}, | |
}); | |
const data = await response.json(); | |
return { ...data.data[0], access_token: accessToken }; | |
} catch (error) { | |
console.error(error); | |
} | |
} |
I can't find it in my code anymore, I think I switched to the full redirect flow for some reason.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do you have the code for
popup.html
anywhere? Seems like I am missing something about how Cloud Functions receives the call back from the OAuth service.