> > In my experience with OAuth, one of the principle issues is that it's less a protocol and more a skeleton of a protocol
> OAuth supports lots of different scenarios.
But it's literally only the skeleton of a protocol ("framework" of a protocol, as the actual spec puts it). That important "Then later on you can take the token and say 'Hey google, somebody gave me this token, who is it?'" part is totally unspecified by OAuth. How do you validate tokens? (Either on the resource-server end, or on the client-end to know if trying to talk to a resource-server is a waste of time, or to know if your post-redirect-uri is getting spoofed):
- OAuth: implementation defined
- most implementations: use our library, which validates the token using undocumented logic
- OIDC: there's a user-info endpoint you can call to, but lots of OAuth authorization servers don't fully implement OIDC, and even if they do, that's a lot of extra round trips
- RFC7523: The token is a JWT, validate the signature and claims... but everyone issues RFC6750 "bearer" tokens, not RFC7523 "urn:ietf:params:oauth:grant-type:jwt-bearer" tokens. But if you just close your eyes and pretend that your 6750 opaque token is actually a 7523 JWT and parse+validate it as such, that'll work most of the time.
So yeah, OAuth is hard because there's no great generic library, because a core part of it is implementation-defined, so you have to either do it yourself or use a specific implementation's library, and none of those libraries work quite the same.
(In the above, and for 99% of folks, "OAuth" = RFC6749+RFC6750)
In all honesty, though, "I have to look at the provider's documentation to determine like 4 URLs to add to a configuration" is not that wild of a thing? How much friction is too much friction?
> How do you validate tokens? (Either on the resource-server end, or on the client-end to know if trying to talk to a resource-server is a waste of time, or to know if your post-redirect-uri is getting spoofed):
At the beginning of the "standard" OAuth flow you can pass in client state. So you generate a signed nonce. Remember, you are starting this flow, so you mix in some user data (avoids CSRF problems), and you are pointing to some HTTPS site.
This is not 100% perfect compared to "remote server has some signing" but it does allow purely local verification of tokens when they are first received, as you can stick stuff into the "state" request parameter (as anyone should be anyways to avoid trickery).
> In all honesty, though, "I have to look at the provider's documentation to determine like 4 URLs to add to a configuration" is not that wild of a thing? How much friction is too much friction?
If it's something dumb/simple like grabbing a JWKS URL and an "issuer" URL, and then doing JWT validation, sure. Maybe you need to make a weird bespoke request to some other endpoint. Maybe you need to implement some uncommon signature verification. It could be anything.
> At the beginning of the "standard" OAuth flow you can pass in client state. So you generate a signed nonce. Remember, you are starting this flow, so you mix in some user data (avoids CSRF problems), and you are pointing to some HTTPS site.
So you redirect the user's browser to the IDP, with the nonce in the URL. The user grabs that nonce from the URL bar, then manually navigates to your redirection-endpoint, putting that nonce and their own fabricated token in the URL.
The server generates the auth code and redirects the user agent to your callback. You exchange that code with the IDP (over HTTPS which yeah that's its own nest of wormy trust) to get back a token. They can't inject a token because you don't get the token from them, just the one time code. If it's opaque you introspect it to validate or you just validate the JWT signature after pulling the keys from the JWKS endpoint. Introspection is standardized and an RFC. The state param is just a fucking session identifier.
All these URLs are defined and provided via the .well-known/openid-configuration endpoint. If your IDP publishes that endpoint correctly, most OAuth2 client libraries Just Work (TM) when pointed at the IDP domain.
Do EITHER of you even use OAuth2 outside of just cargo culting something you found off GitHub?
My apologies, it's been a while and I was forgetting about the authorization-code exchange step. So yes, for the most part "client"s can treat the bearer token as opaque. But "resource server"s absolutely cannot.
> If it's opaque you introspect it to validate or you just validate the JWT signature after pulling the keys from the JWKS endpoint.
If it's a JWT, which it doesn't have to be. OIDC allows the token to either be an RFC 6750 opaque token, or an RFC 7523 JWT, and overwhelmingly implementations use a "bearer" (6750) token. But, most of the time it's a JWT, so as I said, closing your eyes and pretending it's a 7523 token works, and so then you can just pull the keys from the JWKS endpoint.
> Introspection is standardized and an RFC.
In my experience, it is exceedingly rare for an IDP to implement RFC 7662 token introspection
> All these URLs are defined and provided via the .well-known/openid-configuration endpoint.
Yeah, OIDC-discovery is pretty sweet. And if the IDP implements OIDC-discovery, then it probably implements OIDC-core, or at least enough of it that you can use the user-info endpoint, like I mentioned. But I've seen IDPs that don't.
Oh, thank you for reminding me of the well-known endpoint, was having trouble finding it in the OAuth RFCs but I wonder if it's pulled in elsewhere.
> They can't inject a token because you don't get the token from them, just the one time code.
that's not correct in the most common flow? The most common flow involves the user agent providing the information via a GET, so they can theoretically provide a token.
The reason the state parameter is important is because without it, a malicious actor can make a link that goes directly to your system's "oauth step finalized" step, but with their credentials. (Pedantic attack: service has slack push notifications integration, through OAuth. Attacker creates link like https : //service/connect-slack-finalize?token=token-to-attackers-slack that victim clicks on. Without using state, the service will just take the token and stick it into some slack integration. now attacker's slack is getting messages for the victim's account on the service. Cookies mean that the victim's thing is accepted immediately).
.well-known/openid-configuration is specified as part of OpenID Connect (OIDC) Discovery[1]. OIDC is separate from and on-top-of OAuth2. The OIDC specs come through the OpenID Foundation, not through the IETF (so not RFCs). (Also, while what they specify is super useful, they aren't nearly as well written as RFCs tend to be :) )
No, he's right, I was misremembering (assuming "the most common" flow is the "authorization code" flow specified in RFC 6749 §4.1). The user-agent provides a one-time "authorization code" to the client via a GET, and then the client receives that "authorization code" and does its own POST to the IDP to exchange that "authorization code" for the final "access token".
This is me misusing the word "token". Access tokens are gotten via POST, but the one-time code is gotten via GET and, absent usage of things like the state parameter, can easily lead to malicious attacks.
That's something that isn't OAuth2 or your end point is accepting something insane.
Are you talking about the PKCE variant of authorization code flow which is what replaces implicit flows in native apps and SPAs? Because those use code_challenge and code_verifier fields, not the state field. If you're doing all that in the state field with signed nonces you really should move to PKCE.
It's to prevent CSRF attacks. The attacker writes their own client and does half of a login on their own end (getting an authorization-code, but not yet exchanging it for an access-token), and then tricks the end-user to navigate to //service/connect-slack-finalize?code=<attackers-code>&state=<whatever>. But with the state parameter, the client can check the state parameter against a session cookie that it set previously, and say "wait a minute, this is the conclusion of a login from a different browser". Depending on what all session-state the client is keeping track of, it may make sense to sign that state-parameter-nonce to avoid having to remember session state server-side; but the simple case would be to just check whether it == a cookie value.
Using [0] as a reference, I'm talking about Step 3. This is, in my experience, the "normal" way that people are setting up OAuth between 2 services, with a user going through the flow.
[1] includes info on this (see "flawed CSRF protection")
Aha! That makes sense! Yes that can be a problem. We exclusively use a single (our own) IdP so it's less important for us. But good to know as some future feature work will actually make this important.
One thing I could imagine here is a signature flow happening from the sender side where they sign the nonce + the token, to avoid forgery. I don't know what the signature validation looks like in that model though (if you request a public key from the provider, are you requesting that at every request flow? If you're not, now you're adding a request to every normal, non-malicious validation flow)
> Maybe you need to implement some uncommon signature verification. It could be anything.
So my feeling on this is that at the very least OAuth tends to be "well it's going to look like this, but there might be some tweaks".
I have set up OAuth verification with multiple services in the past. The social aspects and the business aspects have always been more complicated, but at least "it's OAuth" (much like "it's REST" for APIs) establishes a core understanding very quickly.
I'm open to saying that we should add more standard stuff to the flow, if it's optional then people who use oauth libraries will end up with a lot of this stuff by default. But I think the status quo is pretty nice, all things considered.
My apologies about "to know if your post-redirect-uri is getting spoofed" and then my example exploit; I was misremembering and forgetting the authorization-code step.
If you're only implementing a "client", then yeah, you can probably get away with never caring about inspecting the token. But if you're implementing a "resource server", then you'll need to, and it's all implementation-defined (but overwhelmingly that implementation is "it's a JWT").
Any friction whatsoever is too much friction. By default, any user should be able to use any provider for any service, with no prearrangement between the service and the provider.
Anything less than that predictably leads to the situation we have now where a very small number of very large providers control identity and authentication for way too fucking many things.
> OAuth supports lots of different scenarios.
But it's literally only the skeleton of a protocol ("framework" of a protocol, as the actual spec puts it). That important "Then later on you can take the token and say 'Hey google, somebody gave me this token, who is it?'" part is totally unspecified by OAuth. How do you validate tokens? (Either on the resource-server end, or on the client-end to know if trying to talk to a resource-server is a waste of time, or to know if your post-redirect-uri is getting spoofed):
- OAuth: implementation defined
- most implementations: use our library, which validates the token using undocumented logic
- OIDC: there's a user-info endpoint you can call to, but lots of OAuth authorization servers don't fully implement OIDC, and even if they do, that's a lot of extra round trips
- RFC7523: The token is a JWT, validate the signature and claims... but everyone issues RFC6750 "bearer" tokens, not RFC7523 "urn:ietf:params:oauth:grant-type:jwt-bearer" tokens. But if you just close your eyes and pretend that your 6750 opaque token is actually a 7523 JWT and parse+validate it as such, that'll work most of the time.
So yeah, OAuth is hard because there's no great generic library, because a core part of it is implementation-defined, so you have to either do it yourself or use a specific implementation's library, and none of those libraries work quite the same.
(In the above, and for 99% of folks, "OAuth" = RFC6749+RFC6750)