First and foremost: what flow (grant type) are you talking about? OAuth, even just OAuth 2.0, is a lot of different things. I'll mostly assume you mean authorization code / PKCE (or Implicit but please don't use Implicit) here, as those are the common flows that people get confused about. If you're using one of the flows where you (the client app) specifically pass the authentication data to the OAuth server - which is probably either the deprecated "Resource Owner Credentials" a.k.a. "password" grant or the user-agnostic, machine-to-machine "Client Credentials" grant - then that is a different story. Neither of those use a redirect URL though, so presumably you don't mean them.
Also, I'm not sure what you mean by "an extra token in the redirect url". Please skip ahead to the blockquotes two sections down if you want to address the literal form of your question.
The weird thing about this question, and about the OAuth [2.0] spec in general, is that it generally assumes people are using OAuth for something that it is not specifically intended for but which, in practice, people use it for constantly. Specifically, OAuth is NOT (by itself) intended for SSO (Single Sign On) and does NOT assume that the client application (your app) and the resource server (the intended audience of the OAuth access token) are the same (in fact, it assumes they aren't)! In other words, unless there is a third-party API that you want to access using the token you get from the OAuth server, you are not using OAuth for the thing it's intended for.
A lot of the confusion around OAuth being an authentication protocol vs. an authorization protocol goes away in the specific (very common) scenario where the client application and the resource server are the same thing or at least have the same owner and mutual trust. In other words, the scenario where you are the expected consumer of the "access token" and there is no third party, just your app and the "authorization server" that your app delegates auth (both authz and authn) to.
However, I slipped another assumption into that last sentence! It's one which the OAuth spec does NOT require and therefore generally assumes to be false: that the resource server (in this case you, who are also the owner of the client application) has chosen an OAuth server which both performs an authentication check on the user and returns a token containing a specific identity. Neither of those is required! They are typical in practice, which means people tend to treat them as "how OAuth works", but OAuth totally allows for authorization servers which merely validate that you're some authorized entity (which could be by means as simple as "you come from a trusted IP address" but is more often stuff like "somebody has, at some point in the past, authenticated from this machine but there is no promise that it's a private machine and the authentication was to authorize access for any user of the machine") and return a token saying "the bearer is authorized to access everything in scope foo
" without any information about who the user is.
People tend to not consider that last paragraph, because "of course I'm not going to trust my app's authentication to a weird OAuth server that is so lax about authentication / unspecific about the user's identity!" and indeed they should not. However, it complicates attempting to understand OAuth in the context of authentication if you forget about that. OAuth allows authorization that is not authentication, and therefore if you pick an OAuth server that reliably performs authentication and returns the result of it, you are using something that is not simply OAuth, something that is more specific than OAuth and it is misleading to say "sure, you can use OAuth for authentication" because the assumptions you are making are not part of the OAuth spec.
can't we treat the landing on redirect url as the proof of authentication instead of requiring an extra Id token
By itself, absolutely not. That would be trivial to spoof; your redirection URL is not secret and there's no reliable way to restrict how / where from somebody gets there. Even if you check the Origin
or Referer
header (or similar) to see whether the user came from your OAuth server, that doesn't work because the user/attacker can just spoof those value using a custom request, inside or outside of the browser.
Also, this inherently fails as a method of authentication because the redirect URL doesn't inherently contain identity information. If your redirect URL is "https://gaurav5430.app.example.com/oauth/redirect", and I fire off a request to that exact URL (e.g. by clicking the link), who am I? You have no idea. Can't realistically consider my authenticated in that situation, can you?
OK fine Mr. Pedantic; obviously I meant "landing on the redirect url, and then I verified the presence and correctness of the state
token or otherwise verified that this specific user had initiated this OAuth process, verified the presence of the code
token and that the authorization code was issued by a trusted OAuth authorization server via successfully exchanging the authorization code for an access token, and successfully parsed the access token to ensure that it refers to a valid user which I was able to do because I'm my own resource server and therefore the intended audience of the access token". Furthermore, obviously I only trust OAuth servers that don't do the weird stuff you talk about in the paragraph above that starts "However" so I know the OAuth server authenticated the user to whatever standard I wanted and returned specific identification of that user!
Oh did you, really, all of that? Great, why didn't you say so?! I'm not a mind-reader, you see, and that's not at all "obvious"; people get this stuff wrong ALL THE TIME so I don't assume[1] it. Anyhow, yes, in that case (note that there are rather a lot of conditions on that case!), you can treat the user completing the OAuth flow (which is NOT the same as "landing on the redirect URL"!) as authenticated.
Bear in mind, though, that some of those conditions may be hard to enforce using pure OAuth.
- For example, suppose you decide you want the user's session to end after an hour... but you allow the OAuth-based "Sign in with Microsoft", and don't bother to perform the OIDC lookup. Unless you also control the Microsoft account in question (e.g. it's a Microsoft 365 account and you control the org and enforce login and session policies), you have no way to know if the user last logged in just now during the OAuth flow, or seven years ago and has been using the same session since. In the latter case, your "session ends after an hour" control is meaningless, because an attacker can probably just refresh the app session using the long-lived Microsoft account session!
- Alternatively, what if you're using OAuth-based "sign in with Google" and the user logs out of Google after the OAuth flow completes but while their session on your app is still valid? Maybe it's a work account and they just don't want to be distracted by work emails (or are trying to stop Google from tracking them everywhere)... or maybe they handed their device to somebody else for a while. In the latter case, you probably want to end the user's session on your app too, because the user now operating the machine is no longer the one who authenticated and the one who authenticated expressly doesn't share accounts with new person. OAuth by itself doesn't support notifying you of this! Instead, your app session will persist until the user explicitly logs out, ends the session some other way, or the session expires (assuming you have an expiry at all), and until one of those events, you won't know that the authenticated user is no longer the active user. Perhaps you're OK with this - it's valid to say "if the user wanted the other person to not have access to their account on my app, they'd have logged out of my app before handing off the device" - but maybe the user assumed that logging out of Google would log them out of everything they logged into with Google. There's a way to do that, but it requires additional work on the app developer's part, and in particular requires that the app not treat one-time OAuth flow completion as persistent authentication the way you are doing.
[1] Honestly, "assume the person designing the system is doing everything right" is literally the opposite of my job as a security engineer; a substantial part of doing this job well is to not make those assumptions and instead verify everything.