I’m a huge fan of the Material-UI component library for React. I’ve used it in professional and personal projects for many years now, and it’s super sweet. Why is it so great?
Two things:
A wide variety of widgets that cover just about any UI you would want in a web site
Excellent documentation
There is also a large community of users so lots of good Stack Overflow questions and answers exist. Occasionally I’ve had to try and make their components do a new bit of magic, and it can be tough to wade through all that knowledge to determine if a specific use case will work. Recent case in point: figuring out how to replicate features of the Gmail To field.
My first step was considering using a chip input library for MUI I found earlier that would add chips to a standard MUI TextInput component. But it appeared that library was not really being maintained any more, and under the hood it used the pre-hook React lifecycle. So maybe not the best choice for new feature work. But I lucked out: there was an open issue in that repo that linked to an example that basically did what I wanted and bonus, it was in TypeScript.
This code sample used the MUI Autocomplete component, which is a solid way to get input from a list of preset options that supports type-ahead. Or it can be completely free-form where you can type anything you want in addition to a preset value. You can also configure it to allow inputting multiple values and then render special markup for those entries, as well as rendering whatever input component you want. All well and good. I was going to use it as the CC field on a page for sending email, and I had certain requirements:
Make sure each entry is a properly-formatted email address
Make sure the domain wasn’t in a blocklist
Do these validations when the user hit the Space key after typing an address
The first two were easy: we already had a regex to check the format of an address that worked for our purposes, plus code elsewhere to check if an address domain was in our blocklist. The tricky part was the Space key. The Autocomplete component has two props for its value: value and inputValue, which are tracked separately. In my configuration the former will have the list of email addresses, and the latter will have the value of the <input> element (in my implementation the list of addresses was actually a list of objects, each with the address and a validity flag).
To capture when the user inputs something, I handle the onChange event. The trigger for it is hitting the Enter key, which is good since I want users to be able to press that key to check the address they just typed. In order to capture any other keystrokes, I needed to handle the onKeydown event for the underlying TextField. The tricky part was telling it to use my custom handler.
The example I was following had a function for the renderInput prop. That function returned a TextField for use as the input portion. It included whatever props that were passed to renderInput, spreading the given object on to the TextField instance. It wasn’t readily apparent that any props were actually being set. Instead I spread my own object that included my key down event handler. And suddenly it became very broken, for a good reason: there were some props that Autocomplete needed to assign to the TextField and I was completely leaving them out.
The solution was to add my handler to the incoming object holding the required props, and assign that to the TextField, like so:
renderInput={(params) => {
const newParms = {...params};
newParams.onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// Check for the Space key and handle it as needed
};
return (
<TextField
{...newParams}
label="CC"
placeholder="Enter email address"
/>
);
}};
After that the regular Autocomplete features all worked and my Space key handler functioned as expected.
It was a bit over 10 years ago that I first wrote about my efforts to create a better version of a long-dead native iOS app that Mozilla wrote to dip their toes into the iOS pool. All their app did was give you access to certain data in your Sync account: bookmarks, history, open tabs, etc. It was perfect for me because I had an iPhone and lots of Firefox bookmarks, having used the browser since 2006.
My desire was to write a web app that worked the same but avoided a really dumb bug in their app, and also learn some new technologies while I looked for a new gig (I had just moved to Seattle, WA). I reached into my depths of creativity and called it the Bookmark Browser. It was a tremendous success. I still use the app today, over 10 years later. But it’s changed a lot in that time, evolving to use newer tools and work with changes to Firefox services. Follow along as I catch up on what’s changed since the last revisit.
Sidebar: I want to first tip my hat to Valérian Galliat who in mid-2021 did a deep dive on his efforts to do the same thing the Bookmark Browser does, which is access data in Firefox Sync from an application that’s not Firefox. I wasn’t able to take advantage of the code he wrote, but it’s a detailed and entertaining journey into what it takes to build a third-party app on top of the Firefox ecosystem (hint: far, far too much). It also inspired me to re-visit my original back end code and see if I could get it working again.
The original 2012 Bookmark Browser implementation was a front end built on jQuery Mobile + KnockoutJS with a back end written in C#. That back end leveraged a client library written to do the heavy cryptographic lifting of getting data out of Sync. Life was grand until 2016, which is when authenticating against Firefox services started becoming cumbersome. Previously my code could log into my Sync account and pull down data immediately, with no other interaction needed. Mozilla was in the process of improving their services and hardening things around the login API. I ended up having to code around those restrictions (e.g. needing a second step in the process to verify a login). The good news was it still worked.
Also in 2016 I started using AngularJS in my day job and really liked it. In early 2018 I decided to convert the front end to that framework. It was also around this time that the Sync access just stopped working. I’d get an ‘Unauthorized’ error when trying to call their login API and couldn’t figure out a way past it. So I switched the back end to accept an upload of a JSON bookmark backup made from the desktop version of Firefox, and an endpoint to fetch that data. It was annoying to have to go through the extra step of uploading a backup whenever I wanted to refresh the bookmark data stored on my phone. But it was worth it to have the app work consistently.
In December of 2020 I decided another tech stack update was in order, because AngularJS was nearly dead and I wanted to switch hosting providers. I re-wrote the front end in React which I had been using for over a year and a half in my day job. The back end was the same C# web service that accepted a bookmark backup file, using the latest 4.x version of .NET. I still prioritized certainty of operation over the convenience of being able to get data directly from Sync. The fun part of this upgrade was figuring out how to have front end components communicate with each other, which was very easy in AngularJS. I decided to use the now-much-improved Context API and the various hooks React now offers. I replicated all functionality and resolve some nagging CSS issues along the way. It was a great improvement. I then moved everything to Azure for hosting in a Windows VM, and Azure DevOps for CI/CD (the VM also hosted this blog and therefore ran the WordPress stack).
The app worked great but as time went on I really, really wanted to figure out how to talk to Sync directly again. Then in early 2022 I found Valérian’s blog series on doing exactly what I wanted to do, purely by chance after a random Google search. As luck would have it, at that time I was considering porting the back end to Node.js, since I wanted to ditch my Azure VM and set everything up in an App Service based on Linux. And he had some JavaScript code! Sadly, when I tried to use it I ran into an error I couldn’t explain while trying to authenticate. But he said he had gotten it to work, which suggested it was possible. I dusted off my original C# web service, updated it to a .NET Core 6 project, and compared that code to the JavaScript Valérian had written to authenticate and access the encrypted contents of Sync storage. I really only found one important difference: a reason identifier for the initial login request.
The first step in the crypto-dance to get data out of Sync using Mozilla’s original BrowserID protocol is to make a login request using your email address, Firefox Accounts password, a verification method, and a reason for the request. In my login API that request boils down to the following, using some helper code for the mechanics of making an outbound request and a class for this particular request:
public class LoginRequest
{
public LoginRequest(Credentials credentials)
{
this.Email = credentials.Email;
this.AuthPW = BinaryHelper.ToHexString(credentials.AuthPW);
this.VerificationMethod = "email";
this.Reason = credentials.Reason;
}
[DataMember(Name="email")]
public string Email { get; private set; }
[DataMember(Name="authPW")]
public string AuthPW { get; private set; }
[DataMember(Name="verificationMethod")]
public string VerificationMethod { get; private set; }
[DataMember(Name="reason")]
public string Reason { get; private set; }
}
Post<LoginRequest, LoginResponse>("account/login" + (keys ? "?keys=true" : ""), loginRequest);
In the original Sync client I used, the LoginRequest class only had the email address and password. And I think what happened is at some point the other two values became required, and Mozilla didn’t really go out of its way to tell third-party developers who maintained apps that authenticated to Sync via code. But Valérian noticed it and his implementation passes in all those pieces of data. So I simply added those two properties to the class and it worked. Sort of. I had to go through some trial and error because apparently there are a couple different verification methods you can use. The reason value always needs to be ‘login’, which avoids any immediate need for verification. The verificationMethod value can be one of these supported values:
email
Sends an email with a confirmation link.
email-2fa
Sends an email with a confirmation code.
email-captcha
Sends an email with an unblock code.
I tried email-2fa and email-captcha but couldn’t get either one to work. I don’t know why, but in the end it didn’t matter. The process now has three steps: step one is making a login request, step two is clicking a link in the email you get to verify the login attempt, and basically establish a valid login session to the Firefox Accounts services, and step three is getting bookmark data. The legacy BrowserID protocol that all of this code uses basically requires a new set of keys be generated each time you want to make requests to Sync storage, because they are very short-lived. So on step three I have to make the same login request, but this time it will pass with flying colors and I simply get an email warning that a successful login was made using your credentials, make sure it was you, etc.
I love J.K. Simmons
As of today, Mozilla still supports their legacy BrowserID protocol and infrastructure for authenticating an account and using their various Sync services. This is very nice of them. But I’m under no illusion that any of it is permanent, and that it could all disappear tomorrow. But I don’t think that’s likely, since my guess is part of Firefox or other Mozilla apps/tools still use that protocol. And so they will support it for a while still. As long as they don’t change it I’ll be able to get bookmark data out of my account and into Bookmark Browser every time. And even if they did finally pull the plug on the BrowserID stuff, I left in the bookmark backup code so I can fall back to that.
Postscript: Mozilla does have a mature OAuth implementation that they put together a while ago, which is obviously a better way to authenticate. But it assumes you have a valid client ID for the initial auth challenge, and that they know the URL to redirect users to once they’ve obtained a token. Apparently they want you to ‘consult’ with them to set it all up, which sounds rather unappealing. As Valérian mentioned and I agree with, Mozilla doesn’t seem to want to make it easy for developers to build apps on top of the Sync ecosystem. Which is sad because a lot of cool things could probably be built.
Update: Turns out ‘a while still’ means up to May of 2024: Mozilla officially started decommissioning an essential piece of the BrowserID protocol. I found this out by trying to update the bookmarks on my phone and getting a strange 404. The endpoint that does cryptographic certificate signing as part of the authentication process was slowly being restricted. It’s now basically gone and so I’m left with my fallback of uploading a Firefox bookmark backup JSON file. When I first learned what was happening I actually considered reaching out to them to see about getting an OAuth setup in place for my app. I haven’t yet but it might be worth a shot. The worst they could do is say No.