Skip to content

Instantly share code, notes, and snippets.

@alexastrum
Last active August 21, 2020 16:54
Show Gist options
  • Save alexastrum/49f5841c7a7cd7160e7849fe182448f8 to your computer and use it in GitHub Desktop.
Save alexastrum/49f5841c7a7cd7160e7849fe182448f8 to your computer and use it in GitHub Desktop.
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import { decode, verify } from "jws";
import axios from "axios";
/** Public webapp key. Required by `signInWithCustomToken` REST API. */
const WEBAPP_APIKEY = "XXXXXXXXXXXXXXXX-XXXXXXX-XXXXXXXXXXXXXX";
/** Schema for Firestore `/devices/{deviceId}` documents. */
interface DeviceData {
nonce?: number;
publicKey: string;
uid: string;
getIdTokenTime?: admin.firestore.Timestamp;
}
interface Device {
id: string;
data: DeviceData;
}
/** Response body of `signInWithCustomToken` REST API. */
interface SignInWithCustomTokenResponse {
idToken: string;
refreshToken: string;
expiresIn: number;
}
/** Looks up and returns a device by its id. */
async function getDevice(id: string): Promise<Device | null> {
const docSnapshot = await deviceCollection.doc(id).get();
return docSnapshot.exists
? { id: docSnapshot.id, data: docSnapshot.data()! }
: null;
}
/** Prints a decoded representation, alongside the JWT to the console. */
function printJWT(message: string, token: string) {
const { header, payload } = decode(token);
console.log(message, header, payload, token);
}
/** Generates, persists and returns a unique (per device) number. */
async function getNextNonce(device: Device) {
const nonce = (device.data.nonce || 0) + 1;
await deviceCollection.doc(device.id).update({
nonce,
getIdTokenTime: admin.firestore.FieldValue.serverTimestamp(),
});
return nonce;
}
/**
* Reformats the base64 encoded key.
* When manually copying the key, some whitespace is changed, making the key non-compliant with the spec.
*/
function normalizePublicKey(pk: string) {
return (
"-----BEGIN PUBLIC KEY-----\n" +
pk.replace(/\\s|-----(BEGIN|END) PUBLIC KEY-----/g, "") +
"\n-----END PUBLIC KEY-----\n"
);
}
admin.initializeApp();
const deviceCollection = admin
.firestore()
.collection("devices") as admin.firestore.CollectionReference<DeviceData>;
/** Generates and returns new Firebase id token for the provided device token. */
exports.getIdToken = functions.https.onRequest(async (req, res) => {
const deviceToken = req.body;
printJWT("Device token: ", deviceToken);
const { header, payload } = decode(deviceToken);
const device = header.kid && (await getDevice(header.kid));
if (!device) {
res.status(401).send({
errorCode: 1,
message: "Unknown device!",
});
return;
}
console.log("Device found: ", device.id, device.data);
// ATECCx08 crypto chips only support ES256.
const alg = "ES256";
// Verify the validity of device-generated JWT.
const verifyResult = verify(
deviceToken,
alg,
normalizePublicKey(device.data.publicKey)
);
if (!verifyResult || header.alg !== alg) {
res.status(401).send({
errorCode: 4,
message: "Invalid JWT!",
});
return;
}
const payloadNonce = Number(JSON.parse(payload).nonce);
let nonce = device.data.nonce || (await getNextNonce(device));
if (payloadNonce !== nonce) {
// Invalid nonce.
// Not necessarily a replay attack, the device might just be out of sync due to a network error.
res.status(401).send({
errorCode: 3,
message: "Invalid nonce!",
nonce,
});
return;
}
if (!device.data.uid) {
// This device is not associated with a user account.
res.status(401).send({
errorCode: 2,
message: "Unclaimed device!",
});
return;
}
const customToken = await admin
.auth()
.createCustomToken(device.data.uid, { deviceId: device.id });
printJWT("Custom token: ", customToken);
const idTokenResponse = await axios.post<SignInWithCustomTokenResponse>(
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=" +
WEBAPP_APIKEY,
{ token: customToken, returnSecureToken: true }
);
const idToken = idTokenResponse.data.idToken;
printJWT("Id token: ", idToken);
res.send({
nonce: await getNextNonce(device),
idToken,
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment