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:
- Configure the Authorization Code Flow.
- Start login with a non-trivial state, e.g.
oauthService.initCodeFlow('/products?page=1&sort=name').
- Complete the login and return to the app.
- Read the state after
tryLoginCodeFlow() (e.g. from OAuthSuccessEvent / the service state).
- 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.
Describe the bug
Custom
state(user state) passed toinitLoginFlow(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 (viaOAuthSuccessEvent.info/ thestateproperty aftertryLogin*) 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:and the composite state is then encoded again when it is appended to the URL:
On the redirect back, the query string is parsed (which URL-decodes once), leaving
nonce<separator>encodeURIComponent(userState).parseStatesplits on the separator but returns the user portion without decoding it:So the app receives
encodeURIComponent(userState)instead ofuserState— i.e. one level of encoding too many. This affects any flow that goes throughcreateLoginUrl(Authorization Code Flow and Implicit Flow).Stackblitz example
Reproducible with this repo's sample app (code flow). Minimal illustration of the round-trip:
To Reproduce
Steps to reproduce the behavior:
oauthService.initCodeFlow('/products?page=1&sort=name').tryLoginCodeFlow()(e.g. fromOAuthSuccessEvent/ the servicestate)./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):
Additional context
The fix is a one-liner: decode the user state once in
parseState: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.