Showing posts with label authentication. Show all posts
Showing posts with label authentication. Show all posts

Thursday, June 22, 2023

The mystery of the broken JWT magic link login URLs on iPhone

My team at work was recently facing a problem where "magic link" login URLs being sent out via SMS (text message) were "broken" when received by iPhone users. Only part of the URL's query string portion was properly rendering as part of the link; the remainder -- despite not being separated by a space, or any URL-invalid characters -- was showing up as plain text:


A magic link in this context is an URL that includes a secure, tamper-evident key which identifies the user, and allows them to log in to the application that sent the link, in lieu of having to enter a password. (This has security pros and cons; that linked article provides a nice summary.)

My team is using JWT as the magic link key. JWTs are encoded into three portions, separated by period characters (remember that, it's important later!): A header; the message payload (including things like the user's ID, and the key's expiration time); and the signature verification.

In our case, only the first portion of the JWT value, the header, was being rendered by iPhone recipients of our SMS message as a part of the clickable hyperlink. The remaining two portions were showing up as plain text. This broke the magic link! While it still directed users to our site, it was unable to log them in.

I spent the day yesterday performing an investigation into why this was happening.

For starters, asking Google about the maximum length of an SMS message yields the answer "160 characters." Here in 2023, as far as regular users are concerned, this is no longer really true. (When's the last time you were composing a text message, and your phone stopped you from sending your message because it was longer than about half a Tweet?) All modern providers use "SMS concatenation" to, behind the scenes, break a long SMS message into multiple parts, and then seamlessly stitch those parts back together into a single message for the recipient.

I hypothesized: Perhaps Apple's implementation of SMS concatenation doesn't work when the URL itself is longer than 160 characters (as our magic link login URLs including a JWT token are)? No; I was able to disprove this by sending myself a text message with such an URL; it arrived in one piece, no problems. 

(As an aside, I started out doing these tests by using the web UI of my work's existing Twilio account to send messages to my personal iPhone's number. This worked fine; I pretty quickly determined, though, that I could more expeditiously test by just using my Mac's Messages app to send messages to my own phone number. This produced the same results, as far as the received message ending up broken or not.)

Perhaps SMS concatenation doesn't work when the query string portion of the URL is longer than 160 characters? No; disproved by sending myself such a link, which once again was delivered in one piece, as expected.

Perhaps the problem is when a single query string key-value pair -- or just a query string value -- is longer than 160 characters? No; I was able to successfully send myself messages (using the string "1234567890" repeatedly as the query string value to achieve the target length) in with such query string values excess of 500 characters in length, no problems.

My testing went on like this. I was consistently able to reproduce the broken link behavior using an actual (Dev environment) magic login link; the behavior of any particular URL being broken or not appeared to be consistent/deterministic, at least. Further, by trimming down certain portions of that URL, the link would be correctly delivered in one piece. 

By testing many message and URL variants, and recording for each one whether it succeeded or failed to deliver properly, along with the lengths of the various portions of the message text and the URL, I was finally able to pin down the problematic behavior. Here it is, in plain English:

For a given query string value: If that value contains any URL-valid punctuation characters (i.e. non-alphanumeric characters): If any portion (or "slice") of that query string value beyond the first portion, when separated/sliced by punctuation characters, is 302 characters or longer, the URL will break (on Apple devices). If all such portions are 301 characters or shorter, the URL will render correctly. 

Recall that JWT values consist of 3 portions -- separated by period characters? This meant that if the token's 2nd (payload) or 3rd (signature) encoded portions were in excess of 301 characters, the resulting link would be broken when delivered to an iPhone. 

(Notably: It's only Apple's handling of SMS messages, in their Messages / iMessage app on iPhone and on Mac, where links render as broken in this particular way. In my testing with Android clients, and with Google Voice, all links that I tested with were delivered correctly, regardless of length!)

Here are a few examples of working and broken URLs (when received by an Apple client). To save space (and to make this post less ugly!), instead of actually spelling out URL portions of 300+ characters, I'll represent such portions with the number of characters in that portion. The following links, when delivered to and viewed on an iPhone, or in Apple's Messages app on a Mac:

https://example.com?key=400 (OK; there are no punctuation characters in the query param value)

https://example.com?key=10.302 (BROKEN; the second portion of the query param value is longer than 301 characters)

https://example.com?key=301.301_301 (OK; no portion of the query param value is longer than 301 characters)

https://example.com?key=200~200.400 (BROKEN; the 2nd portion is ok, but the 3rd potion is longer than 301 characters)

https://example.com?key=400-50 (OK; only the first portion of the query param value is longer than 301 characters, and that doesn't manifest the problem)

https://example.com?key1=400&key2=400 (OK; the both query param values here consist only of "first portions", which don't manifest the problem)

To work around this problem -- and to produce links that are some what less nasty-looking on clients that render the entire URL -- I'm planning on making a pair of changes to our magic login tokens:

1. Reducing the payload content to "essential" values only. Namely, the user's email address, and an expiration date/time value. This will cut down on the middle "payload" portion of the JWT.

2. Using HS256 instead of RS256 as the signing algorithm. For our specific application and usage scenario, HS256 will provide sufficient security; but HS256 signature values are significantly shorter in length. 

All of the aforementioned testing was done in June 2023 using an iPhone 12 running iOS 16.5.1; and a MacBook Pro running MacOS Ventura 13.4.  Perhaps Apple will address this issue in future software versions? (But if this particular bug isn't at the top of their priority list, I certainly can understand why not. ☺)

Hopefully this post may be helpful to any of y'all out there who are researching why your SMS messages that include JWT magic link login URLs (or other long URLs including long query string values) being delivered to iPhone clients aren't rendering properly!

Monday, September 08, 2008

How to: Get Firefox to discard a cached set of basic authentication credentials

While using Firefox 3 to work with a web application that uses basic authentication for access control earlier today, I wanted to be able to log in to the application as a different user account without needing to close and restart Firefox in order to get it to stop automatically resending the authentication data from my original login.  (After logging in to a site via basic authentication, Firefox will automatically continue to resend the credentials from that login on any subsequent requests to that same site, until the browser is shut down.)

After hunting around for a while looking for a Firefox extension that would do this, I discovered a way to do this built into Firefox itself: In the Tools menu, select Clear Private Data; then in the Clear Private Data dialog that appears, check the Authenticated Sessions checkbox, clear all other checkboxes, and click the Clear Private Data Now button.

ClearPrivateDataAuthenticatedSessions

This will get Firefox to "throw away" its saved basic authentication credentials from the current session, and allow you to log in as another user, without having to close Firefox.  I've found this to be handy when I have a lot of Firefox tabs open, and don't want to shut down Firefox (and close all of my open tabs in the process) just to "log out" of the basic authentication session that I have open in a particular tab.

Wednesday, August 16, 2006

Avoid being prompted for credentials when accessing Windows Integrated Authentication sites using Firefox

I recently authored a simple ASP.NET internal website at my office to act as a repository for proposed enhancements for an upcoming release of our product. (Visual Studio .NET 2005 made authoring the site really quick and easy, such that the small amount of dev time I needed to put in to create the site was worth the gain in usability over a solution like Excel or Access.)

Since only internal employees would be using the site, I decided to set it up to use the Windows Integrated Authentication provided by IIS 5, so users of the site would automatically be logged in using their domain credentials (rather than being prompted for a set of credentials).

IIS 5 Authentication Methods Dialog

This worked great when accessing the site in Internet Explorer. However, when using Firefox, I would get prompted by Firefox for a set of credentials when first accessing the site. Entering my domain username and password did allow me to access the site successfully.

I wanted to avoid the login prompt with Firefox, so I Googled for firefox integrated authentication prompt. This turned up a couple of good results that pointed me to the solution:

  • In the Firefox Location bar, type about:config and press enter.
  • In the Filter bar that appears, type Network.automatic-ntlm-auth.trusted-uris and press enter.
  • Under "Preference Name", double-click on Network.automatic-ntlm-auth.trusted-uris, and enter the name of the web server machine in the dialog that appears.

Now users are able to access the site without being prompted for credentials -- Firefox uses Integrated Authentication and automatically uses the user's domain credentials to log in.