OIDC can be relatively straight-forward (that is just a few JSON REST calls) if the provider isn't configured in a restrictive way. The .well-known/openid-configuration endpoint is quite helpful. Exchanging username+password (optionally with OTP) for a token is an option in the standard. The issue is that lots of deployments are quite restrictive "for security".

OIDC is only barely more complicated than the minimum viable option for establishing someone's identity based on the word of a trusted third party.

User shows up at your login flow. You assign them a big random number identifying their user session (this is your "state")

User indicates an identity provider they'd like to use. You probably have a short list you trust.

You ask that provider for the configuration data.

You generate a big random number, that identifies this log in attempt as unique. This is your "nonce".

You send the user, along with the state and nonce, to the trusted third party. (at their "authorization endpoint")

The user proves to the trusted third party they are who they say they are. This isn't your problem.

The user comes back to you with a claimed state and a code (a big random number assigned by the trusted third party).

You check that the user's claimed state matches the state that you assigned them. This ensures that you end up authenticating the same user session as the one that started the login.

You then reach out to the third party directly (to their token endpoint), with state and code in hand, and ask them "yo, a user session with this state just claimed you sent them to me with this code. Who are they?".

And then the trusted third party sends back a token attesting "They are so and so".

The one superfluous step is that, according the spec, you're supposed to then verify the signature of that token. It is unclear to me why this is in the spec, since I just made an https request to the trusted third party. The entire security model here has assumed that trusted third party is trusted.