One thing we did at a company (we had significantly more than a million monthly logins) was to host our own OAuth2 OIDC server and use the (serverside) Cognito Admin API as a user management system.
The Admin API lets you create, update, delete users as well as mint tokens and all of that good stuff.
When a user would log into our custom login front-end, we would send the credentials to our identity server, which would then try log in with Cognito via the admin API. Cognito would send back a JWT which we would essentially throw away and reissue a new token from our identity server for the user session.
This gets complex when you start trying to manage things like resetting the password where you need the cognito token and not your token.
Additionally, we found that Cognito had a bunch of embedded functionality that you cannot reconfigure. We had to migrate our users from our existing user stores into Cognito and we decided to trigger a migration when a user logged in. We did this because we don't have the user passwords in plaintext, only when they log in so if we were to automatically dump everyone into cognito we would have had to force everyone to reset their passwords.
It turns out that Cognito has a built in email verification system (send a code and all that) - awesome but when you use the admin API and add a user that already has a verified email address, it still dispatches the verification email.
We had to set up lambdas to intercept the email and check if the user has a certain tag. It was a nightmare.
Anyway, Auth0 is more expensive but easier as everything is customisable. Same with Okta.