I am implementing a firebase realtime database on Wear OS for an accompanying app connected to an Android device and I was wondering what are the best practices for authenticating the user on a wear watch. It is not very convenient to enter a email and password on small watch screens. Is it possible to pass a firebase authorization token through the wear os data layer and if so, how would you use the token from the Android device to authenticate the user on the wear watch?
Thank you, Donny
The documentation covers the different authentication approaches you could use.
Ultimately, you will need at least a web-based method to authenticate the watch as you can't guarantee that the user will have your companion app installed or that the watch is not connected to an iOS device.
You have two approaches available to you (that I can think of):
In this method, you perform the following steps:
RemoteIntent
to open it for them)const functions = require('firebase-functions');
const sha256 = (s) => require('crypto').createHash('sha256').update(s).digest('base64');
const lazyFirebaseAdmin = () => {
const admin = require('firebase-admin');
try {
admin.app();
} catch {
admin.initializeApp();
}
return admin;
}
const createUserAuthCode = async (uid) => {
const chars = "0123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; // omitted O, I, l
let code = "", charsLen = chars.length;
for (let i=0; i<6; i++)
code += chars[Math.floor(Math.random() * charsLen)];
const encoded = sha256(code);
await lazyFirebaseAdmin()
.firestore()
.collection('_server/auth/userCodes')
.doc(encoded)
.create({
created: admin.firestore.FieldValue.serverTimestamp(),
uid
});
return code;
}
const validateUserAuthCode = async (code) => {
const encoded = sha256(code);
const codeRef = lazyFirebaseAdmin()
.firestore()
.collection('_server/auth/userCodes')
.doc(encoded);
const snapshot = await codeRef.get();
if (!snapshot.exists)
return null; // not found
const { uid, created } = snapshot.data();
await codeRef.delete();
if (created.toMillis() < Date.now() - (2 * 60 * 1000)) {
return null; // too old
}
return uid || null;
}
const getDeviceCode = functions.https.onCall(async (data, context) => {
if (context.app === undefined) { // If you want to use Firebase App Check to mitigate abuse
throw new HttpsError(
'failed-precondition',
'Unrecognized caller');
}
if (!context.auth) {
throw new HttpsError(
'failed-precondition',
'You must be authenticated to request a device code');
}
try {
return {
code: await createUserAuthCode(context.auth.uid)
};
} catch (error) {
throw new HttpsError(
'unknown',
'Couldn\'t generate device code',
{ message: error.code || error.message }
);
}
});
const exchangeDeviceCode = functions.https.onRequest(async (req, res) => {
if (req.method !== "GET") {
console.log("Rejected unexpected " + req.method + " request");
res.status(405)
.set("Allow", "GET")
.end();
return;
}
const code = req.query.code;
if (typeof code !== "string") {
res.status(400)
.json({ message: "Missing code param" });
return;
}
try {
const uid = await validateUserAuthCode(code);
const token = await admin.auth()
.createCustomToken(uid, {
isDeviceToken: true // by having this, you can prevent the watch
// auth tokens from doing privileged actions
});
const response = await fetch({
url: "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=[API_KEY]", // TODO: Replace with Web API key
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, returnSecureToken: true })
});
// idToken - Firebase ID token (access token)
// refreshToken - refresh token for this device authentication token
// expiresIn - number of seconds to ID token expiry
res
.status(response.status)
.set("Content-Type", "application/json")
.send(response.text());
} catch (err) {
res.status(500)
.json({ error: "Encountered unexpected error" });
}
});
On the client side, you'd call the first function (after signing in) using either:
// Java
var getDeviceCodeFunc = FirebaseFunctions.getInstance().getHttpsCallable("getDeviceCode")
getDeviceCodeFunc.call()
.addOnCompleteListener({ task ->
if (task.isSuccessful()) {
// got code!
} else {
// failed!
}
});
// Web/JavaScript
const getDeviceCode = firebase.functions().httpsCallable("getDeviceCode");
const code = await getDeviceCode();
Then once the user has put in the code, send it off to
GET https://us-central1-[PROJECT_ID].cloudfunctions.net/exchangeDeviceCode?code=[TYPED_CODE]
In this method, you perform the following steps:
sendAuthorizationRequest()
flow https://wear.googleapis.com/3p_auth/com.your.package.name
with the GET parameters accessToken
and refreshToken
.Note: This is probably overkill for what you are trying to do. But if you really don't want someone having to type in a code on their watch it is available as an option. You could use oauth2-server
to just proxy issuing Firebase ID tokens (access tokens).