Skip to content

Custom state is double URI-encoded in Authorization Code / Implicit flow (parseState doesn't decode userState) #1506

Description

@earshinov

Describe the bug

Custom state (user state) passed to initLoginFlow(state) / initCodeFlow(state) comes back percent-encoded after the redirect. The state is URL-encoded twice on the way out but decoded only once on the way back, so the value delivered to the app (via OAuthSuccessEvent.info / the state property after tryLogin*) is still encoded.

Root cause is an encode/decode asymmetry in oauth-service.ts:

  • In createLoginUrl, the user state is first encoded into the composite state:

    state = nonce + this.config.nonceStateSeparator + encodeURIComponent(state);

    and the composite state is then encoded again when it is appended to the URL:

    url += '...&state=' + encodeURIComponent(state) + '...';
  • On the redirect back, the query string is parsed (which URL-decodes once), leaving nonce<separator>encodeURIComponent(userState).

  • parseState splits on the separator but returns the user portion without decoding it:

    private parseState(state: string): [string, string] {
      let nonce = state;
      let userState = '';
      if (state) {
        const idx = state.indexOf(this.config.nonceStateSeparator);
        if (idx > -1) {
          nonce = state.substr(0, idx);
          userState = state.substr(idx + this.config.nonceStateSeparator.length);
        }
      }
      return [nonce, userState]; // 👈userState is still encodeURIComponent(userState)
    }

So the app receives encodeURIComponent(userState) instead of userState — i.e. one level of encoding too many. This affects any flow that goes through createLoginUrl (Authorization Code Flow and Implicit Flow).

Stackblitz example

Reproducible with this repo's sample app (code flow). Minimal illustration of the round-trip:

const original = '/products?page=1&sort=name';
const url = await (service as any).createLoginUrl(original);
// URLSearchParams decodes once, exactly like the code-flow return does:
const stateParam = new URL(url).searchParams.get('state');
const [, userState] = (service as any).parseState(stateParam);

// Expected: '/products?page=1&sort=name'
// Actual:   '/products%3Fpage%3D1%26sort%3Dname'
console.log(userState);

To Reproduce

Steps to reproduce the behavior:

  1. Configure the Authorization Code Flow.
  2. Start login with a non-trivial state, e.g. oauthService.initCodeFlow('/products?page=1&sort=name').
  3. Complete the login and return to the app.
  4. Read the state after tryLoginCodeFlow() (e.g. from OAuthSuccessEvent / the service state).
  5. The value is percent-encoded (/products%3Fpage%3D1%26sort%3Dname) instead of the original string.

Expected behavior

The state returned to the application should equal the string that was passed in (/products?page=1&sort=name) — encoding applied on the way out should be fully reversed on the way back.

Desktop (please complete the following information):

  • OS: any (environment-independent)
  • Browser: any (Chrome, Firefox, Edge, Safari)
  • Version: any

Additional context

The fix is a one-liner: decode the user state once in parseState:

return [nonce, decodeURIComponent(userState)];

This is the same underlying problem previously reported in #783 ("Double URI encoding on additionnalState"), which was closed as more-info-needed.

I have a fix and will submit a PR shortly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions