Last month, we started to get concerning bug reports from our users that they couldn’t authenticate with Google directly inside of the Pragli desktop application. Here’s the error message that they were seeing:
You are trying to sign in from a browser or app that doesn’t allow us to keep your account secure. Try using a different browser.
We knew that Google had deprecated Oauth authentication for non-standard browsers a year ago, but we had no idea that Electron (based on Chromium) was on the chopping block as well.
This means that our desktop application could no longer authenticate with Google directly inside of the application. We now needed to navigate the user to a supported browser to properly authenticate. I couldn't find a resource to help me solve this problem, so I ended up experimenting with different authentication architectures until I resolved the issue.
In this post, I show how to implement Google authentication for your Electron application without running into any browser restrictions. I use Firebase, React and Google Cloud Functions, but other technologies should have similar implementations.
General idea
Here’s a rough outline of how to authenticate your desktop application:
- Generate a one-time random code within the Electron application when a user initiates a sign in
- Listen for an authentication code in a publicly accessible database, using that one-time random code (more on this later)
- Open the user’s default browser, passing along the one-time code
- Once in the user’s browser, redirect to Google Oauth
- Once you’re redirected back from Google Oauth with the session information, generate an authentication token for arbitrary clients to sign in as the user
- Associate the authentication token with your one-time code in your datastore
- The Electron application receives the authentication token via its listener in Step #2 and signs the user in
Step 1: Generating a random token
There’s no easy way to directly pass data from a browser to an Electron application.
Therefore, we need to use a datastore (Firebase in our case) that is accessible by both the browser and the Electron application to pass the authentication information from the browser, which is handling the authentication, to the Electron application, which needs to sign in.
To uniquely identify a sign in attempt, we also need to generate a random code that the supported browser can use to send the Electron application the user session information/authentication code to sign in. Within the Electron application, generate this one-time code with the uuid()
module.
const signIn = () => {
const id = uuid()
// Rest of implementation
})
Step 2 : Listen for the authentication code
Listen for any authentication information that is passed through Firebase with the .on()
function.
Note: The to-auth-codes/${id}
route needs to be readable/writeable by any unauthenticated client. For Pragli, we judged that the security concerns for this approach were minimal.
const signIn = () => {
const id = uuid()
const oneTimeCodeRef = firebase.database().ref(`ot-auth-codes/${id}`)
oneTimeCodeRef.on(‘value’, async snapshot => {
const authToken = snapshot.val()
// Rest of implementation
})
})
Step 3: Redirect the user to the browser
Navigate the user to the browser to complete the Google authentication. We created a simple route /desktop-sign-in
to initiate the authentication. Make sure that you pass along your one-time use code, so the browser can pass the authentication details back to Electron once the authentication finishes.
const signIn = () => {
const id = uuid()
const oneTimeCodeRef = firebase.database().ref(`ot-auth-codes/${id}`)
oneTimeCodeRef.on(‘value’, async snapshot => {
const authToken = snapshot.val()
// Rest of implementation
})
const googleLink = `/desktop-sign-in?ot-auth-code=${id}`
window.open(googleLink, ‘_blank’)
})
Step 4: Google authentication
At the /desktop-sign-in
page, redirect to Google for authentication. In our case, Firebase made this dead simple with the signInWithRedirect()
function. The service transparently handles setting the Oauth redirect URL and creating a fully populated User object.
componentDidMount() {
const provider = new firebase.auth.GoogleAuthProvider()
firebase.auth().signInWithRedirect(provider)
}
Step 5: Generate the authentication token
Once Google redirects back to your application, recover the Firebase user object with the getRedirectResult()
function.
To generate an authentication token that Electron can use to sign in as the user, you need to use the admin Firebase library. Unfortunately, you can’t use the regular client-side Firebase library to generate a token - even if you’re already authenticated as the user.
To use the admin Firebase library, you need a secure server environment. I prefer to use Google Cloud Functions to keep our infrastructure simple. But regardless of the server that you select, you also need to send the backend the following information;
- The one-time use code
- A user ID token for the server to validate that the sending request comes from the correct, authenticated user
Browser Client
async componentDidMount() {
const result = await firebase.auth().getRedirectResult()
if (!result) {
firebase.auth().signInWithRedirect(provider)
} else {
console.log(“Grabbed the user”, result.user)
if (!result.user) {
return
}
const params = new URLSearchParams(window.location.search)
const token = await result.user.getIdToken()
const code = params.get(“ot-auth-code”)
const response = await fetch(`${getFirebaseDomain()}/create-auth-token?ot-auth-code=${code}&id-token=${token}`)
await response.json()
}
}
Server (Google Cloud Function)
Since you’re executing a cross-origin request from client to a server in Google Cloud Functions, make sure that you wrap the request with the CORS node module to ensure that the correct Access-Control-Cross-Origin
headers are set.
const admin = require(‘firebase-admin’)
const cors = require(‘cors’)({
origin: true
})
admin.initializeApp()
exports.createAuthToken = functions.https.onRequest((request, response) => {
cors(request, response, async () => {
const query = request.query
const oneTimeCode = query[“ot-auth-code”]
const idToken = query[“id-token”]
const decodedToken = await admin.auth().verifyIdToken(idToken)
const uid = decodedToken.uid
const authToken = await admin.auth().createCustomToken(uid)
console.log(“Authentication token”, authToken)
await admin.database().ref(`ot-auth-codes/${oneTimeCode}`).set(authToken)
return response.status(200).send({
token: authToken
})
})
})
Step 6: Associate the auth token with the one-time code
Once you’ve generated the auth token, you can easily associate the token with your one-time code with the .set()
function using the Firebase admin library.
exports.createAuthToken = functions.https.onRequest((request, response) => {
// Other implementation
const authToken = await admin.auth().createCustomToken(uid)
await admin.database().ref(`ot-auth-codes/${oneTimeCode}`).set(authToken)
return response.status(200).send({
token: authToken
})
})
Step 7: Use the auth token to sign in as a user in Electron
In Step #2, you set your Electron application to listen for updates to the the one-time use token route in Firebase. Once the server sets the auth token at that Firebase route, simply grab the auth token and use it to sign in.
const signIn = () => {
const id = uuid()
const oneTimeCodeRef = firebase.database().ref(`ot-auth-codes/${id}`)
oneTimeCodeRef.on(‘value’, async snapshot => {
const authToken = snapshot.val()
const credential = await firebase.auth().signInWithCustomToken(authToken)
})
const googleLink = `/desktop-sign-in?ot-auth-code=${id}`
window.open(googleLink, ‘_blank’)
})
Another authentication strategy
We used a publicly accessible datastore with Firebase to communicate from the browser to the Electron application.
You can also facilitate the communication between the browser and the desktop app by spinning up a local server upon boot of the Electron app. Then, the local server simply writes to a known filesystem location, and the Electron app can poll for changes at that location to get the authentication token.
Concluding thoughts
The recent Google authentication restrictions have been a huge pain for desktop developers. The trickiest part is managing the communication channel from the browser back to the Electron application. Hopefully this tutorial helped in planning your migration to browser-based authentication.
What’s Pragli?
I built a virtual office for remote teams to frictionlessly chat and feel present with each other. If you’re interested in using the product with your team, learn more here or reach out to me on Twitter.
Top comments (0)