DEV Community

Cover image for 5 Lessons Learned Connecting Every IdP to OIDC
CKMo
CKMo

Posted on • Edited on • Originally published at pomerium.com

5 Lessons Learned Connecting Every IdP to OIDC

Developers may be familiar with the need to add authentication and authorization to applications. Today, this usually involves integrating some form of identity provider (IdP) for single sign-on (SSO) purposes via Open ID Connect (OIDC).

But OAuth is already very hard and eats up a lot of dev time. Consider how difficult it is to configure OIDC, which is built on top of OAuth? We thought that since this process is a big headache for developers, “Wouldn’t it be nice if OIDC's manually complicated and easily broken process was just done for you for any IdP?”

Implementing OIDC is an adventure alright

While it’s certainly nice for developers, getting there was a nightmare. Below we included 5 lessons we learned simplifying OIDC for all developers.

Take a deep breath with us and let’s go.

Lesson 1: Too Many Standards Means No Standard

As always, there’s a relevant XKCD.

Let’s work a bit backwards first: why do we want to integrate with IdPs? Well, in the OIDC setup, the Relying Party (RP, or service provider) wants authentication (and authorization, but we can’t cover everything), and IdPs are a great way to authenticate the user trying to access the service.

To accomplish this, the RP usually wants three things per OIDC standards (bear with us, we know we’re simplifying this):

  • Access Token — allows RP application to access API/User Info endpoint
  • Refresh Token — for getting a new Access token
  • Identity Token — caches User profile information and provides it to client app

While all the IdPs end up giving you these three tokens, what we did not expect was that they would give you these three tokens in wildly different forms. And by that, we don’t mean that they don’t always give it to you as a JSON Web Token (JWT) — which, incidentally, is true in some cases — but that each token would have a nonstandard form of giving you the information you wanted.

Some IdPs put everything in the ID Token. Other IdPs put everything in the User Info endpoint and make you query the API. Then some IdPs want to do a combination of these things, giving you some information in the ID Token and then make you query the rest from their API. This results in a lot of different sources of truth to standardize!

And we wanted to provide one gateway that allows for all these IdPs to plug and play for developers trying to configure OIDC.

Standardizing All ID Tokens for One Gateway

There were so many ways these three tokens alone looked differently across IdPs, but ID Tokens were definitely the biggest wildcard. Almost all of them were providing user identities in different ways. That’s a problem seeing as obtaining this information from IdPs is more or less the entire point.

One of these is not like the others.

So, earlier we mentioned that ID Tokens cache the user’s profile information and provides it to the client app. It’s a major part of the OIDC specification because well, part of authenticating someone is getting information about them. The ID tokens are supposed to provide a bunch of basic information about the user which the service can use to authenticate the user.

We were naïve enough to think the ID Tokens would look like passports: standardized across the IdPs. Instead, each IdP acted like a state, issuing their ID Tokens like driver’s licenses: while the data was all there, no two tokens were presented in the same format.

Put their product owners in a room and let them fight.

Here’s some examples of this madness:

In many cases, simply using a library to attempt OIDC would fail since obviously none of the IdPs provide things in an actual OIDC format. The result was spending devtime to write code for an extra layer to transform each individual ID token into an actual standard form before parsing it for the OIDC library.

Deep Breath.

Lesson 2: Most IdPs Don’t Even Try OIDC

It turns out not every single IdP supports OIDC. GitHub is an example — they don’t even OIDC, so we had to integrate with GitHub using OAuth.

There are many IdPs that simply don’t support OIDC, or gave us an API that is close to OIDC but different enough that the implementation does not result in OIDC. There were various challenges associated with trying to understand how that would work, and imagine our surprise when it turns out the answer is: “Oh, we don’t actually support OIDC.”

On the flip side, one IdP stands out as the easiest to integrate with and most closely aligns with OIDC standards. Drum roll…

It’s Okta.

(It’s Okta.)

Lesson 3: Directory Sync at Scale

The problem with scale is that you don’t think about it until boom, use case. Well, turns out our larger users (organizations with over 10k users) had a lot more groups in their directories than we initially thought to test for.

What did we think? Well, maybe around 100-300 groups? That seems reasonable, right? It stands to reason that groups being a way to categorize users would result in the group count < user count… right?

What do our users with over 10k users actually have? 10k groups. (Yeah. That was our reaction too!)

Some directories can have groups within groups (introducing group recursion, a different headache), and the OIDC authentication process would need to add or update all of the groups at once during directory sync. That’s a lot of data.

Somehow.

Together, all at once.

Deep Breath.

Directory Sync.

Look, IdPs, please understand that you need to support groups without doing a full directory sync! When each user’s info reaches 1MB and there’s >10k users, that’s an enormous data problem when you’re trying to do a full sync each time.

Groups info shouldn’t be in constant flux; there can’t be that many rockstar employees fulfilling so many different functions their groups info changes all the time (and if there is, just create a rockstar user group).

Azure AD actually figured this out with a delta changes feature. This feature means directory sync only calls the information that changed (so less than 1KB per user). Yes, it was hard to integrate, but it scales! The easy solutions don’t! The easy solutions are not lightweight at all!

Please do lightweight directory syncs.

Lesson 4: Where Should Data Be Placed?

More data problems, but elsewhere.

We noticed that IdPs doing OIDC are trying to jam tons of user info data into query string parameters. This being the param which OIDC is passing around to communicate and make all the authn and authz happen.

Unfortunately, this method has a size limit of about 2,000 characters, and you can realistically only pass so much data within that limit. Again, some of our users are organizations with greater than 10,000 groups. That’s a chonky string.

Being a team of engineers, the solution seemed obvious: put the data elsewhere.

Oh look, user info endpoints exist! All we need to do is query the user info endpoint and then there’s no data limit! Alright, new feature for our OIDC-IdP interface —

What’s that, not every IdP supports user info endpoints?

(As always, you set out with an idea and then reality hits you with the design decisions of others.)

Deep Breath.

Lesson 5: Keeping It Manageable

After our newfound epiphany that none of the IdPs will stick to the OIDC standard and all of them were doing their own thing, our devs were side-eyeing the “maintenance and upkeep” bucket with fear.

To save our sanity, we created a directory provider interface.

This allows developers implementing OIDC to adapt the quirks and weirdness of each IdP to their Pomerium instance. We then moved our existing directory providers to a separate repository so that implementation of directory providers is not tied to open-source Pomerium Core.

The result is achieving the original goal (helping developers simplify OIDC with any IdP) while keeping it manageable.

Deep Breath.

Closing Thoughts — Saving Devtime and Sanity

For those who want to take a crack at it yourselves, good luck! We do have some words for IdPs though:

Hey IdPs, it would be really nice if you stick to the OIDC standard. But here’s a few additional asks from our engineering team:

  • Please test your OIDC implementation against common SDKs so it works out of the box. Based on our understanding, everyone will need the Cognito hack to implement Cognito in OIDC. (Maybe it works fine in java, but we’re using the off-the-shelf Go library to do this.)

  • Please realize you all need to support group sync without doing a full directory sync. It’s a data intensive process and very annoying to maintain.

  • Please give people a way to add arbitrary directory data to claims, and everyone would be a lot happier. This would also make your product a lot more lightweight and easier to integrate with other products

A rising tide lifts all boats, and OIDC is very important in how things are done today. Developers shouldn’t be stuck configuring it for hours on end! Making sure that it’s as simple as possible for users to implement is key.

After all that work, we’re proud to say that developers can easily add OIDC to any resource via Pomerium. Whether you’re spinning up a new application or trying to add access control to a legacy service, OIDC authentication and authorization is just an SDK away. You can check out our open-source Github Repository or give Pomerium a try today!

Top comments (0)