return to table of content

OAuth from First Principles

taeric
22 replies
23h31m

I'm still not entirely clear on if I should abandon implicit flow for a static site to get credentials. Easy enough to switch it to the CODE flow, but that gets you a refresh token with AWS and that feels a bit more risky to have on the client side.

mooreds
5 replies
22h51m

I'm still not entirely clear on if I should abandon implicit flow for a static site to get credentials.

The answer is to threat model.

What's the risk of an access token falling into the hands of someone who has got malicious JS into your webpage?

If it is a public API or not risky, maybe implicit is okay.

There's more on the attacks and mitigations here: https://fusionauth.io/blog/whats-wrong-with-implicit-grant

taeric
4 replies
18h24m

My point is implicit or code, the tokens are on the client. And if code, refresh tokens will be there, too.

If malicious code on client is a big risk, I really don't see how the flow matters at all. Specifically if the only computer I have is on the client.

mooreds
3 replies
17h12m

There are a couple of ways to keep tokens on the client that prevent malicious code from accessing them.

- use HTTPOnly secure cookies. These will not be accessible to malicious JavaScript but can only be sent to servers on the same domain. Well, I guess if there was an exploit that let JS break the sandbox and access cookies, they could be accessible, but I think we can trust the browser vendors around this kind of security. This approach is widely supported, but does expose the token to physical exfiltration (that is, if someone on the browser opens up devtools, they can see the cookie with the token in it).

- store the tokens in memory. As far as I know, malicious JS code can't rummage around in memory. This works for SPAs, but does break if the user refreshes the page.

- bind the token to the client cryptographically using DPoP. This is a newish standard and isn't as widely accepted, but means you can store the token anywhere, since there's a signing operation tied to the browser.

All of these can work and have different tradeoffs.

taeric
2 replies
14h52m

I can't use http only cookies, because I have no server to set them. I'm using JavaScript on the client to do the post to get the tokens. Storing in memory is mostly fine, though I do want them in a cookie. May be able to skip that.

I will look into the dpop thing. A bit limited as I'm using AWS cognito.

I confess my thread model is such that I think I am fine with storing token in local storage. Would like to be using standard practice, though.

mooreds
1 replies
5h57m

Yeah, if you don't have a backend server, you're pretty limited in your options.

Sounds like you have spent some time thinking through your threat model and arrived at what works within the constraints of your system.

Here's a good resource on general threat modeling I found: https://github.com/segmentio/threat-modeling-training

taeric
0 replies
4h21m

I'd be a liar if I said I had fully modeled things. I apologize for making it sound like I hadn't given it any thought.

I was hoping I had missed some updated guidance on how to manage tokens. Way too much of the documentation I was finding on OpenAPI and OAuth seemed to be aspirational and referencing things that hadn't come to be, yet. It has gotten rather frustrating.

Thanks for the link, it is a surprisingly fun topic to read on.

apitman
5 replies
19h25m

That's an interesting question, but what exactly is the threat model here? A rogue extension somehow reading the token? Is it stored anywhere? AFAIK generally the concern with access tokens is that they get gleaned from logs or MITM, not pulled out of the browser's memory.

taeric
4 replies
18h27m

Sure, but there are also good habits? I get not having the token in the url is a logs protection. I'm less clear on storing it in local storage.

apitman
3 replies
17h6m

I'm not sure we're talking about the same thing. Basically I'm saying that generally the URL bar and logs are considered more vulnerable than variables in memory.

However, I think I would trust a value that's been transmitted from memory more than one that's been stored in localStorage but not transmitted, because the latter is trivial to grab with an extension.

I'm not aware of any way for an extension to grab a variable from memory unless it knows how to access the variable itself from JS. This makes me wonder if there could be a security practice where you purposefully only store sensitive data like OAuth2 tokens in IIFEs in order to prevent extensions from accessing them. There's got to be some prior art on this.

Anyway, thanks for bringing up the question. It's been a useful thought experiment.

taeric
2 replies
14h57m

Makes sense and I agree there. The code flow has the advantage of not being in the url. I'm still unclear if I have a best practice on how to store the refresh token on a static website client. Feels off using local storage.

apitman
1 replies
13h33m

If you're in a situation where you would otherwise be using the implicit flow, can't you just throw the refresh token away? That should approximate the implicit flow.

taeric
0 replies
7h6m

This is what I'm currently doing. If my thread model includes malicious client, though, it doesn't work. Trivial to intercept.

CuriouslyC
5 replies
23h28m

You can do stuff to keep the lifetime down on the refresh token.

taeric
4 replies
23h20m

I mean, sure. I can also revoke them and such. Still not clear if that is much better than just doing the implicit flow.

littlecranky67
3 replies
23h16m

Problem is that Cognito impicit flow wont issue refresh tokens and limits acces token lifetime to 24h

taeric
2 replies
23h8m

Right, but that seems preferable if I'm doing this all client side? Yes, an access token could get leaked in the URL. But sending the refresh token to the client feels far more dangerous.

littlecranky67
1 replies
22h43m

Yeah but that also means your users will have to re-login every 24h. Other implementations (i.e. Keycloak) issue refresh tokens on the implicit flow.

taeric
0 replies
22h20m

Is it ok practice to store the refresh token in local storage, then?

candiddevmike
3 replies
23h25m

You (typically) only get a refresh token if you ask for offline access in your request scope AFAIK. It's not the default (online is).

taeric
2 replies
23h21m

For AWS Cognito's Token endpoint? Where is that documented? I'm also not seeing that behavior, as I don't have that in my scope and I do get a refresh token if I do the CODE flow.

candiddevmike
1 replies
23h18m

I'm referring to the openid standard I guess[1], it looks like AWS Cognito does something different (they don't support offline_access, but they always issue a refresh_token from what I'm reading).

1 - https://openid.net/specs/openid-connect-core-1_0.html#Offlin...

taeric
0 replies
22h17m

Yeah, if you do a CODE flow, you get three tokens. Implicit is only access.

They at least have decent support to guard api access using these.

ultimoo
9 replies
21h40m

If you’re building a new SaaS today would you simply implement this natively in your stack or go with a vendor like auth0?

lknuth
2 replies
21h29m

I hve just implemented this after we moved away from SuperTokens. My takeaway is that its easier than you'd think (there are libraries that do interaction with the SSO provider for you) and you can fine tune it to your liking (for example, more involved account linking).

If you're starting out though, probably go for a SaaS in the beginning. But be sure to have monitoring for pricing and an option to close account creation, these things can become expensive fast.

tgma
1 replies
20h26m

I am curious what your issue with Supertokens was?

lknuth
0 replies
9h53m

Many. We used the NodeJS Version of it, which has pretty poor error handling. When it breaks, it breaks hard (runtime errors with no message or stack trace)

Security. You can not deactivate certain unsave mechanisms. For example, if you send it an ID token, it will not verify the aid claim, allowing Anny valid token from the same SSO provider.

API stability. We're consuming their API from a mobile app. But every major version (about five a year) changed the REST API without backward compatibility or versioning. Its fine if you use their lib and keep parity, but that's really only possible on the web.

All of this was with their self hosted offering, I haven't tried their hosted one.

pphysch
0 replies
19h50m

If you aren't in a rush, it's worth learning and the implementation won't be too big.

n2d4
0 replies
19h21m

Stack Auth is trying to solve exactly this — open-source, developer-friendly, and reasonably priced managed auth. That way, you don't have to worry about OAuth but still aren't locked into a specific vendor.

The downside is that we only support Next.js for now (unless you're fine with using the REST API), but we're gonna change that soon.

mooreds
0 replies
18h29m

My opinion, as someone who works for a company with both a free and paid auth software option: it depends.

If you only need minimal auth functionality and you have one app, go with a built-in library (devise for rails, etc etc).

If you need other features:

- MFA

- other OAuth grants for API authentication

- SSO like SAML and OIDC

or you have more than one application, then the effort you put into using a SaaS service or standing up an independent identity server (depending on your needs and budget) is a better solution.

Worth acknowledging that auth is pretty sticky, so whatever solution you pick is one that you'll be using for a while (assuming the SaaS is successful).

Auth0 as a choice is good for some scenarios (their free plan covers 7k MAUs which is a lot for a hobby project), but understand the limits and consider alternatives. Here is a page from my employer with alternatives to consider: https://fusionauth.io/guides/auth0-alternatives

apitman
0 replies
19h4m

Build it yourself. Then throw away your implementation and use a battle-tested library.

jiggawatts
8 replies
19h51m

And this is why it takes me a solid minute to log in to my CRM web app to submit my timesheets for the day, a task that takes only 5 seconds.

apitman
7 replies
19h23m

Sounds like a bad implementation. There's nothing inherent to OAuth2 that makes it slow (thought the redirects to create a latency floor). If you want a good experience try logging in to my website https://takingnames.io/. Once you have an identity on LastLogin it's lightning fast.

jiggawatts
6 replies
19h6m

"Nothing inherently bad" other than:

1. Tens of kilobytes of JS that is executed exactly once, so is not amenable to JIT optimisation.

2. A strictly sequential series of operations with zero parallelism.

3. Separate flows for each access token, so apps with multiple APIs will have multiple such sequential flows. Thanks to JS being single-threaded, these will almost certainly run in sequence instead of in parallel.

4. Lazy IdPs that have their core infrastructure in only the US region, so international users eat 300ms per round trip.

5. More round-trips than necessary. Microsoft Entra especially uses both HTTP 1.1 and HTTP/2 instead of HTTP/3, TLS 1.2 at best, and uses about half a dozen distinct DNS domains for one login flow. E.g.: "aadcdn.msftauth.net", "login.live.com", "aadcdn.msftauthimages.net", "login.microsoftonline.com", and the web app URLs you're actually trying to access and then the separate API URLs because only SPA apps exist these days.

6. Heaven help you if you have some sort of enterprise system that the IdP needs to delegate to, such as your own internal MFA system, some Oracle Identity product, or whatever.

I've seen multi-minute login flows that literally cannot run faster than that, no matter what.

This is industry-wide. I stopped using chatgpt.com because it makes me re-authenticate daily (why!?) and it's soooooooo slow. AWS notoriously has its authentication infrastructure only in the US. Microsoft supports regional-local auth servers, but only one region, and the default used to be the US and can't be changed once set. Etc, etc...

aaronpk
4 replies
17h50m

"nothing inherently bad" other than:

(a list of things that are specifically bad implementations)

In my demos the OAuth flow completes so fast you can't even tell it happened, you don't even see the address bar change to the IdP the second time you do a flow when you already have a session there.

jiggawatts
2 replies
17h26m

Are you in close physical proximity to your servers? Do you access your own application multiple times per day? Then you're testing an atypical scenario of unusually low network latency and pre-cached resources.

At scale, you can't put everything into one domain because of performance bottlenecks and deployment considerations. All of the big providers -- the ones actually used by the majority of users -- do this kind of thing.

This argument of "you're holding it wrong" doesn't convince me when practically every day I interact with Fortune 500 orgs and have to wait tens of seconds to a minute or more for the browser to stop bouncing around between multiple data centres scattered around the globe.

apitman
1 replies
16h55m

Big providers have more resources than anyone when it comes to having their servers close to users and optimizing performance. They can afford things like AnyCast networks and custom DNS servers for things like Geo routing. Just because they don't doesn't mean they can't.

you can't put everything into one domain because of performance bottlenecks

What specifically are you referring to here?

jiggawatts
0 replies
15h39m

If you look at my original comment in this thread, I mentioned that to log in to something like Microsoft 365 via Azure Entra ID, the browser has to connect to a bunch of distinct DNS domains. About half of these are CDNs serving the JavaScript, images, etc... For example, customers can upload their own corporate logos and wallpapers and that has to be served up.

Just about every aspect of a CDN is very different to an IdP server. A CDN is large volumes of static content, not-security-critical, slowly changing, etc... Conversely the API is security-critical, can't be securely served "from the edge", needs rapid software changes when vulnerabilities are found, etc...

So providers split them such that the bulk of the traffic goes to a CDN-only domain distributed out to cache boxes in third-party telco sites and the OAuth protocol goes to an application server hosted in a small number of secure data centres.

To the end user this means that now the browser needs at least two HTTPS connections, with DNS lookups (including CDN CNAME chasing!), TCP 3-way handshake, HTTPS protocol negotiation, etc...

This also can't be efficiently done as some sort of pre-flight thing in the browser either because it's all served from different domains and is IdP-controlled. If I click on some "myapp.com" and it redirects to "login.idp.com" then it's that page that tells the browser to go to "cdn.idp.com" to retrieve the JavaScript or whatever that's needed to process the login.

It's all sequential steps, each one of which bounces around the planet looking up DNS or whatnot.

"It's fast for me!" says the developer sitting in the same city as both their servers and the IdP, connected on gigabit fibre.

Try this flow from Australia and see how fast it is.

apitman
0 replies
17h13m

This guy OAuths. Trust me.

apitman
0 replies
19h1m

The only thing I'm really tempted to defend here is the multi-domain thing, because I'm not aware of another way to set cookies for multiple domains in a single flow, but maybe consolidate your services under a single domain like google does? Minus youtube.com of course, which is fair.

hn_throwaway_99
8 replies
23h2m

I thought it was somewhat funny that all of the points highlighted in the "Attack #1: Big Head's credentials are exposed" section are exactly how Plaid works.

Spivak
2 replies
22h57m

Yep, Plaid is put in a rough position of having to make something work across a bunch of financial institutions that can't or won't enable applications to have secure access. Plaid's existence and popularity is the canary that banks are way behind on features that users want.

tcoff91
1 replies
19h2m

most banks do actually support oauth now. Plaid mostly uses OAuth now.

bpicolo
0 replies
16h56m

"Most" is a strong phrase. Some financial institutions are taking it the opposite direction, like Fidelity which only allows a completely different partner now (Akoya).

ycombinatrix
0 replies
17h34m

move fast and break things

tomjakubowski
0 replies
22h47m

Not true anymore, at least for some banks. On connecting to Plaid, Chase provides an oauth flow, and Plaid never sees your credentials.

Plaid announced this level of integration with Chase in 2018. Unsure when it was implemented.

https://plaid.com/blog/chase/

mooreds
0 replies
22h55m

This is why I avoid plaid at all costs. Hopefully FAPI and some of the banking open standards will (eventually) help with this.

arianvanp
0 replies
14h23m

There is even a whole extension of oauth optimised for banking. https://oauth.net/fapi/

Perhaps US needs a PSD-2 like regulation to force innovation on this front?

apitman
0 replies
19h6m

Mint was the same way back in the day. I think Chase still doesn't support hardware tokens or authenticator apps. It's insane that banks are somehow the furthest behind in many security best practices.

apitman
8 replies
19h32m

This is fantastic. I've implemented OAuth2 4-5 times over the past few years. It seams overly complicated at first, but over time I've come to understand more of the vulnerabilities each piece of complexity exists to mitigate. Every time I've thought I could do it simpler, I eventually discovered a vulnerability in my approach. You get basically all that knowledge here in a concise blog post. This is going to be the first thing I link anyone to if they're interested in learning OAuth.

One gripe from Attack #3[0]:

The solution is to require Pied Piper to register all possible redirect URIs first. Then, Hooli should refuse to redirect to any other domain.

There's actually a another (better IMO) solution to this problem, though to date it's rarely used. Instead of requiring client registration, you can simply let clients provide their own client_id, with the caveat that it has to be a URI, and a strict prefix of the redirect_uri. Then you can show it to the user so they know exactly where the token is being sent, but you also cut out a huge chunk of complexity from your system. I learned about it here[1].

[0]: https://stack-auth.com/blog/oauth-from-first-principles#atta...

[1]: https://aaronparecki.com/2018/07/07/7/oauth-for-the-open-web

apitman
2 replies
17h23m

most major websites

Is this true? Do you know of any major ones offhand? That would be surprising to me.

Thanks for sharing [0]. I found its discussion of shared subdomain cookies useful. However, I believe all the vulnerabilities in the OAuth section would be mitigated by using PKCE and not using the implicit flow, even if you leave the open redirect. Am I missing anything there?

As for open redirects in general, it is an important problem. As an authorization server, if you want to protect against clients that might have an open redirect (and as you indicate eventually one will), while still using the simple scheme I mentioned above, I can think of a few options:

1. Require the client_id to exactly match the redirect_uri instead of just a prefix. This is probably the most secure, but can result in ugly client IDs shown to the user, like "example.com/oauth2/callback". Of course clients can control that and make it something prettier if they want.

2. Strip any query params from the redirect_uri, and document this behavior. That should handle most cases, but it's always possible clients implement an open redirect in the path itself somehow. You could also check for strings like "http", but at some point there's only so much you can do.

3. Require clients to implement client metadata[1], so you can get back to exact string matches for redirect_uri. This is a very new standard, and also doesn't work for localhost clients.

[0]: https://sec.okta.com/articles/2021/02/stealing-oauth-tokens-...

[1]: https://datatracker.ietf.org/doc/html/draft-parecki-oauth-cl...

apitman
0 replies
16h24m

Does that count as an open redirect? It gives a big fat declaration where you're coming from and where you're going to, and requires the user to choose.

I agree some nonzero number of users would click to continue when they shouldn't while doing an OAuth flow. Thanks for the example.

xyzzy123
1 replies
15h21m

That's neat.

Tho if you do this then clients need a distinct client id per redirect uri domain, mostly this is non consequential and a good thing but I think it has some ux implications like seeing multiple consent screens vs just 1 if you had multiple redirect uris registered against a single client id.

apitman
0 replies
13h31m

What's a situation where you would need multiple redirect domains? I generally work with small scale stuff.

Kinrany
1 replies
17h46m

There's actually another (better IMO) solution to this problem

Is it standardized?

apitman
0 replies
17h15m

You don't have to violate anything in RFC6749, so technically yes. As I said it's not widely adopted, but it's also not difficult to implement. Generally OAuth2 client libraries require you to enter the client_id which you get during registration. Instead just tell them to use the URI of their app. You can see an example of such instructions for my LastLogin project here: https://lastlogin.io/developers/. It's just a short paragraph.

shreddit
7 replies
1d

Quite funny they ask “why would you want to reinvent the wheel” when they just did that…

And why do most of these “Auth0” replacements only implement nextjs?

n2d4
4 replies
23h39m

We gotta start somewhere, and Next.js is popular right now — we're working on some cool non-Next stuff though (eg. auth proxies that can provide user info to any server, which we can hopefully launch within the next weeks).

Regardless, the focus of this blog post is on OAuth, not Stack Auth =) I appreciate the feedback though.

shreddit
2 replies
23h13m

Yes, the blog post is very informative (although the images do not scale well on mobile).

My previous post was based on the phrase “open-source Auth0” (in your blog post) and we use Auth0 (and don’t like it). But all of out apps are react, not nextjs

3dbrows
1 replies
21h57m

I’m currently evaluating Auth0 and other similar services with a view to migrating from Cognito. May I ask what you don’t like about it?

grinich
0 replies
18h49m

(plugging my startup- hope that’s ok!)

we’ve had lots of folks migrate from Cognito to WorkOS. Lots of more features, modern API, and better extensibility.

More here: https://workos.com/docs/migrate/aws-cognito

chairmansteve
0 replies
21h39m

Thanks for this, I will work through it.

If you happen to be looking for future blog posts:

One thing that would help me and probaby a lot of other people is to show the flow using API calls in Postman.

Currently I am planning to reverse engineer example code in an unfamiliar framework...

mooreds
0 replies
18h27m

And why do most of these “Auth0” replacements only implement nextjs?

Probably because it's hard to support different languages. Even if all the replacements support OIDC (not a given) there are still subtle differences in implementation and integration.

That said, check out FusionAuth! We have over 25 quickstarts covering a variety of languages and scenarios. (I'm employed by them.)

https://fusionauth.io/docs/quickstarts/

We're free if you don't need advanced features and run it yourself, as documented here: https://fusionauth.io/blog/fusionauth-on-fly-io

apitman
0 replies
19h19m

Wheels should be reinvented once in a while, just to make sure there's not a better way to do it. They just shouldn't be reinvented by everyone all the time when there are perfectly good solutions available.

0cf8612b2e1e
3 replies
23h52m

This is rendering poorly on my phone. Had to enable desktop mode to see the diagrams which are unwise cut off.

n2d4
1 replies
23h43m

Thanks for the catch, fixed it now!

Sn4ppl
0 replies
23h30m

Now it looks great

codetrotter
0 replies
23h50m

Some of the pictures stick out beyond the right side of the screen, and scrolling felt slightly sluggish(?), but aside from that I found the page to look pretty ok in Safari on iPhone.

ilrwbwrkhv
2 replies
17h51m

Nice article but once again, using "open source" as a marketing gimmick is just a bad practice.

If you really want to do that, have self hosting, contribution and other things front and center and don't take VC funding.

n2d4
1 replies
17h33m

It's really not just a marketing gimmick — it's the sole reason why we are building Stack Auth, instead of going for one of the existing proprietary managed auth solutions. Trust me, we wouldn't spend time building this if we didn't believe in the one thing that makes us different.

I'll quote myself from elsewhere:

Both Zai and I care a lot about FOSS — we also believe that open-source business models work, and that most proprietary devtools will slowly but surely be replaced by open-source alternatives. Our monetization strategy is very similar to Supabase — build in the open, and then charge for hosting and support. Also, we reject any investors that don't commit to the same beliefs.

Fortunately, nowadays a lot of VCs understand this (including YC, who has a 10+ year history of investing in FOSS companies); we make sure that the others stay far away from our cap table.

ilrwbwrkhv
0 replies
17h22m

They will eventually want you to have an exit. And there lies the problem. You should have gone for something like futo if you wanted the money. Right now there is an ideological dissonance in what you're doing. Same with supabase et al.

The only reason the real open source is what the world is built on, is because there is a guarantee that they will never need an exit and the community will always exist.

bityard
2 replies
21h2m

I love the Silicon Valley references. One of the funniest shows ever made. I don't see it talked about much on HN, maybe it tends to hit a little too close to home?

TacticalCoder
1 replies
19h40m

Both Silicon Valley and The IT Crowd (the british version) are amazing. Although not comedy I also strongly recommend Halt and Catch Fire.

mooreds
0 replies
18h26m

We've used both in our doc. It's a great way to be playful and nod to geek culture.

Still waiting for the cease and desist :)

racl101
1 replies
22h44m

I would love to learn all architecture lessons in the context of Silicon Valley.

tonymet
0 replies
20h7m

with Bighead, and possibly Jinjiang , exclusively

mooreds
0 replies
18h27m

To be fair, they recommend reading this doc at the end of their article.

mattgreenrocks
1 replies
23h54m

Thank you for writing this up. Helps me understand all the steps and why they exist.

mannyv
0 replies
22h30m

The 'why' is what makes this post good. Lots of posts just saw 'do this.' But to learn you should really know the 'why.'

klabb3
1 replies
18h22m

Do regular people verify domains? It feels like the entire domain based trust model has been eroded by in-app browsers eliding the chrome for “simplicity” and even bank/visa official payment gateways that are redirecting you all over weird partner domains and microservice endpoints. Plus of course lack of education and mantras that mortals can follow at all times.

If users don’t verify domains, isn’t good old phishing more effective, like the $5 hammer in that xkcd?

n2d4
0 replies
18h9m

Regular people may not but password managers do. Maybe not everyone, but at least security-conscious people would grow suspicious when their password manager doesn't autofill credentials on a page, and wouldn't immediately jump to manually entering it.

(SSO also reduces the attack surface of phishing, though of course then the attacker just has to phish the identity provider's credentials instead.)

candiddevmike
1 replies
1d

No mention of JARM unfortunately.

apitman
0 replies
19h21m

DPoP would top my list, but honestly I'm glad they stuck to the core stuff.

bcherny
1 replies
21h51m

The site seems to have been hugged to death. Does anyone have a mirror link?

stronglikedan
0 replies
21h37m

Man I wish I would have run across this when you first posted the blog. I just spent the last week learning all of this in pieces and by trial and error! Great writeup!

rguldener
0 replies
22h34m

Very cool! I love how you build on the need for each step one by one.

We implemented (O)Auth for 250+ APIs with Nango and wrote about our experience here: https://www.nango.dev/blog/why-is-oauth-still-hard

palk
0 replies
18h32m

Probably the best OAuth tutorial on the internet. Also it's amazing marketing, it just gets scarier every paragraph. By the end you're 100% put off implementing OAuth yourself haha

lxe
0 replies
23h25m

I haven't really dove deep into oauth flow since 2015 or so until I started working on a little side project choosing to go with AWS Cognito, and the whole PKCE part of the flow was new to me. This explains them well.

focusedone
0 replies
22h4m

This is great!