My question and is how should state be encoded into the state vs stored in local storage? In other words why put the redirect URL into the state if we are going to compare the state value against something in local storage? Why not just encode a nonce and then if that matches, load the redirect URL from local storage?
You are absolutely correct. Storing state properties (like the redirect URL) both in the state message and locally doesn’t really make sense. Most approaches will do something along these lines:
Generate and store a nonce locally (cookies/session/localstorage), along with any desired state data (like the redirect URL). Use the nonce as a state in the protocol message. If the returned state matches the stored nonce, accept the OAuth2 message and fetch the corresponding state data from storage. This is the approach used by Auth0.js
Generate and store a nonce locally. Encode any desired state (like the redirect URL) along with the nonce in a protected message (that will need to be encrypted/signed to avoid tampering). In the response processing, unprotect the message, getting the nonce and other properties stored. Validate that the included nonce matches what was stored locally and, if so, accept the OAuth2 message.
Is there any reason to go with option 2 there? It seems more complicated. One variation of your 2 that I thought of to avoid tampering would be to compare the entire state that comes back (nonce + state) to what was stored and then only act if it matches in its entirety. That being said, I still don’t get why that is better than option 1.
Not that I can think of other than personal preferences. The .Net Core 2 OIDC stack does it, but it is a very modular piece of software where you can swap many things, and they handle the “correlation” aspect of this (making sure that the request was initiated by the same user for which the response is received) separately.