This article is the best explanation I've found after a LOT of searching (h/t to @rickchristie for linking to it in a comment to another answer).
Summary
Relying on plain OAuth 2.0 for authentication is dangerous if you (the client) are just trusting ANY valid access token you receive that's associated with that user as a reliable indicator that you're receiving the request from the actual user, without knowing whether that access token was generated by the user trying to log into your site or if it was generated by the user logging into some other (malicious) website/app.
With OpenID Connect, you can just look inside the JWT ID token provided by incoming requests and see if that token was generated by the user logging into your app or if they were logging into some other web app.
But if you're using the "authorization code flow", i.e. the user is being directed to send your back-end the authorization code and your back-end is trading that for the access token, and then your back-end is saving a record of those access tokens, or if you are issuing the user a signed JWT that contains the access token you generated on their behalf, then you can just check any incoming request to see if the provided access token is one that you have generated, thereby preventing malicious apps from impersonating your user with an access token that was created when the user logged into the malicious app.
If you do that check, you don't need to use OpenID Connect to avoid that kind of attack. But it seems that for some use-cases (e.g. pure-frontend auth libraries) this method of saving and checking access tokens is either not possible or not convenient, and it's for those use-cases that OpenID Connect is necessary.
Key quotes from the article
- "So [when using only OAuth 2.0] we wind up in the situation where any site the user logs into with their Facebook account can impersonate that user at any other site that accepts Facebook logins through the client-side flow (the default for Facebook). The less common server-side flow is more secure, but more complicated."
- "The client has no way to tell who the authorization server thought it was issuing that access token to."