“Code Monkey think maybe manager want to write god damned login page himself.”
— Jonathan Coulton

Truer words may never have been spoken. Set up a database, build a signup page, enforce some stupid password rules, salt and hash it, now create a login page, oops when does login expire, oops need a forgot password page. Or, integrate with an “identity provider” that requires you to use some crazy third party library with who-knows-what dependencies awash with unhelpful and vaguely-defined concepts like “claims” and “entities” and “level of assurance.” Dude, life is too short for this crap — I just want to know who’s logged into my app. And maybe (maaaaaaybe) sort them into a few buckets like “admins” or “read-only.”
Side note: You know who had this figured out? Windows NT and IIS in 1998. Check one box and your site is secured with the same credentials you use for everything else on the domain. Bliss.
Happily, excepting a few projects in Azure meant for my family, since retiring from corporate life I haven’t had much cause to worry about this stuff. But it came up this week during a conversation about “under-the-desk” servers. Every single company I’ve ever known has a few of these … servers that run little slapped-together homegrown enterprise tools that somehow become mission-critical. These days they aren’t really under somebody’s desk, they’re up in a virtual machine somewhere, but no difference. Vortex, Pokey, Nitro and Flora are just a few under-the-desk machines I’ve supported over the years.
It occurred to me that there was one of those tools that I’ve written again and again, and might be fun to put together as a small open source project. But before I tell you about that, I’m going to have to up my Java WebServer game a bit with … yes, wait for it … login. Sigh.
Social Login to the Rescue
I’m not doing it from scratch though, that’s for damn sure. It turns out that despite its (jargon-heavy history) OAuth2 has become a pretty ubiquitous standard for integrating with third-party services. It’s actually the same technology we explored in Part 3 of my SMART on FHIR tutorial. And while typically the standard is used to support data integration (e.g., by brokering access to personal health data in the FHIR case), it can serve as a pretty nifty basis for “social login” as well. This is what’s going on behind all of the “Login with Facebook/Google/etc.” buttons you see around the web. Seems like it just might do the trick for us.
Which service(s) you rely on depends on your use case, but an OAuth2-based implementation gives you a ton of options: Google, Facebook, Github, Microsoft / Office365, Twitter, Apple, we could go on for a long time here. There are minor differences in implementation, but not enough to cause much trouble. We’ll take a look at a few to tease out that diversity.
The bulk of the code here is just over 300 lines in OAuth2Login.java. Another 60 or so lines integrates it into WebServer.java, and that’s the lot. Not that much at the end of the day, but the jargon-to-code ratio is annoyingly high. I’ve tried to simplify things by limiting the exploration to a web-based flow without some more esoteric features; once the basics make sense, adding these back in is pretty easy.
To try the code yourself, first you’ll need to register a provider application. Let’s use GitHub, because they make it relatively simple:
- Log into your GitHub account.
- Under your profile icon, choose Settings / Developer Settings / OAuth Apps.
- Click “Register a New Application.”
- Fill out the required fields, using https://localhost:3000/__oauth2_redirect for the “redirect” URL.
- Generate a new Client Secret.
- Copy your Client ID and Secret and save them for later.
To run the code, you’ll need access to a machine with a recent JDK, git and maven:
git clone https://github.com/seanno/shutdownhook.git
cd shutdownhook/toolbox
mvn clean package install
cd ../sdweb
mvn clean package
vi config-ex.json # edit this file to include your GitHub Client ID and Secret
java -cp target/sdweb-1.0-SNAPSHOT-jar-with-dependencies.jar com.shutdownhook.sdweb.App config-ex.json
Finally, point your browser to https://localhost:3000/echo?msg=w00t. You’ll need to acknowledge the scary self-signed certificate, and then you should be redirected to GitHub where you’ll need to log in and approve access. When you’re redirected back from GitHub, you should see your GitHub login and email address. Success!
OK, but what actually happened?
Let’s see what’s going on under the covers here. We’ve got a web site, and want to delegate login duties to GitHub. We’ll need to understand (and implement) three things:
1. App registration & scopes
GitHub needs to know about our web site ahead of time. Each service does this differently, but for all of them we provide a “redirect URL” to our site, and receive an app (client) identifier and secret in return. After our user successfully logs in, their browser lands back on our configured “redirect” URL. This is an important piece of the security provided by OAuth2 — GitHub will only send credentials back to that HTTPS URL on our preconfigured domain. Our site certificate helps ensure that the URL is actually “us.”
In most cases we’ll also have to register the “scope” of data our application wants to access. Typically this describes a particular class of information relevant to the service. For example, FHIR servers offer scopes relevant to patient health records, while Twitter understands scopes for reader and writing tweets, muting or following users, and so on. For our login use case, the “scope” is limited to just an email address and/or login name, so we don’t have to worry about it too much. But it’s good to understand what’s going on, otherwise the documentation is going to seem pretty weird and convoluted.
2. The authentication redirect
Back in our app, we need a way to know that our user is logged in, usually a cookie of some sort. When that’s missing, we start the login process by redirecting the browser to GitHub’s “authorization” URL with a few query string parameters:
- The Client ID we got when we registered our site.
- Our redirect URL. Most services allow you to register multiple allowable redirect URLs; this parameter lets you specify which one should be used for this request.
- Our desired scopes. This needs to be either the same as, or a subset of, the scopes that were registered with your app.
- A random state parameter that we’ll remember as part of the user’s browser session. This acts as a cross-site request forgery token for the exchange.
- A few other random constant bits.
When the browser hits this page, GitHub presents the user with their login page (if needed), then asks them for permission to share the requested data (scopes) with your application. If all goes well, they redirect the browser back to your site via the redirect URL.
3. Swapping codes for tokens
GitHub sends back this auth information as more query string parameters:
- If something bad happened (like the user declined to authorize your app), an error parameter (and possibly error_description and error_uri) will give you a sense of what happened.
- Your state parameter will be returned to you. If this doesn’t match what you sent to auth, abort!
- A single-use code that’s just one step away from a complete login.
Your last job is to swap that code for an access token (and other things). Do this by sending it in a POST to GitHub’s token URL, along with some proof that you are really you (your client_secret) and other noise. The response will include an access_token and you, my friend, are authenticated and authorized. Store this away in your own authorization cookie or other session state and feel free to go about your business, done and dusted.
Oh wait there’s more
For many OAuth2 use cases, you don’t need anything beyond the access_token. It allows you to call APIs on the user’s behalf, so fetching health conditions from FHIR or saving documents in Google Drive is good to go. But in our social login case, we almost certainly need some kind of unique login ID or email address, so that we can recognize the user across visits. We also need this if we’re going to differentiate between types of access — e.g., maybe bob@example.com is granted admin rights on your site.
Depending on which provider you’re using, things here can start to get a little muddy. But most of the time we can use an add-on standard called OpenID Connect. First, add “openid email” to your scope parameter. This tells the provider that you’re doing an OpenID Connect login and would like to receive identifying information about the user, including their email. There are more options available, but that’s good enough for us.
Now when you call the provider’s token endpoint, in addition to the access_token you’ll receive an id_token that has the goods. The id_token is in JWT format — three Base64Url-encoded strings separated by dots. Since we’ve received our id_token from a trusted source, all we need to do is decode the middle section, which is its own JSON object that amongst other things includes:
- A provider-unique token in the field sub, and
- Their email in the field email.
Woohoo, we’re done! Except wait, not all OAuth2 providers support OpenID Connect. Like GitHub. Argh.
Don’t lose heart; not all is lost
GitHub doesn’t support OpenID Connect, but it does have an API to fetch the user’s profile. And the profile includes an email address as long as you include “user:email” in your scope. We just have to use our access_token to make an extra call.
This is actually is a good example of the edge cases developers are always dealing with in the real world. Even when you do all of this correctly, a GitHub user can block their email address from public visibility. In that case it’s not included in the /user endpoint — but you can still find it using /user/emails (after all, they said you could have it when they agreed to your scope!). Always something.
Who can keep track of all these URLs?
There are tons of OAuth2 providers out there; I’ve collected deets on a few to get you started. If you use my code, all you’ll need is the Client ID and Secret; but the links are useful in case you want to roll your own. Good luck!
Provider | URLs | Scopes | Notes |
openid email | |||
openid email | |||
Microsoft Entra | openid email | AKA Azure Active Directory, this also works for Microsoft 365 and consumer accounts. | |
Github | user:email | See code for fetching login and email. Either a "Github app" or "OAuth app" can be used for social login. | |
Amazon | profile | See code for fetching login and email. |
This stuff can change quickly, so please let me know if you find a mistake or would like me to add a new provider!
We ignored a lot!
Our implementation here is honestly pretty simple. It gets the job done — and that’s awesome — but there’s a ton more hiding underneath if you want to go there. Just for example:
PKCE
We’ve implemented the “confidential client” version of OAuth2. This version relies on the ability to keep the client secret safe, which as a server-side app we’re able to do. But OAuth2 can also be used by “public clients” that can’t keep secrets safe (e.g., because they run entirely in a browser). These apps used to use something called the implicit flow, which counts only on the security of your HTTPS certificate and predefined redirect URLs for security. That ground is a little shaky though, so current implementations have added a feature called “Proof Key for Code Exchange.” This serves as kind of the inverse of the state parameter — proving to the server that the client asking to swap a code is legit.
Refresh Tokens
Our authorization URLs include a parameter access_type with the value online. This means that we’re just going to use our access_token for the life of the current session (or as long as the server allows us to, whichever is shorter). For Social Login this is more that enough, because we don’t really need the access_token for API access at all, except maybe in the GitHub or Amazon case where we use it once to fetch profile information.
If instead we passed offline for this parameter (and assuming the provider allows it), we would also receive a refresh_token in exchange for our code. We can use this to “refresh” our access_token whenever it expires — enabling scenarios that require long-lived API access without the security risk of forever-tokens. Maybe for something like sending an alert whenever a Google Drive file is updated. Nice!
Device Authorization Flow
There’s even an OAuth2 flow for devices that don’t have a browser or keyboard at all! The device displays a code, and then just polls waiting for an access_token to be available while the user enters the code on their phone or laptop or other device altogether. While they don’t tend to use OAuth2, you’ve almost certainly used a very similar experience authorizing apps like Netflix on a Smart TV.
OK, so now we can log in. Next!
Thanks to my snazzy new OAuth2 implementation, I can easily secure my web apps without ever writing “create table users……” again. And that is, most certainly, a worthwhile outcome. Now I can get back to that under-the-desk-app, which I’ll look forward to sharing in the coming weeks. Woot!