From 04ab6b23c585c152b9d26c48e72ecde3ed47e6ae Mon Sep 17 00:00:00 2001 From: Shashank Bhatotia Date: Tue, 28 Jul 2026 15:21:58 +0530 Subject: [PATCH] fix(iOS): omit state when additionalParameters.state is null --- .changeset/tidy-pianos-shave.md | 5 +++++ docs/docs/usage/config.md | 1 + packages/react-native-app-auth/index.d.ts | 2 ++ packages/react-native-app-auth/ios/RNAppAuth.m | 12 +++++++++++- 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-pianos-shave.md diff --git a/.changeset/tidy-pianos-shave.md b/.changeset/tidy-pianos-shave.md new file mode 100644 index 000000000..ab63136aa --- /dev/null +++ b/.changeset/tidy-pianos-shave.md @@ -0,0 +1,5 @@ +--- +'react-native-app-auth': patch +--- + +Fix iOS sending `NSNull` as the OAuth `state` when `additionalParameters.state` is null. diff --git a/docs/docs/usage/config.md b/docs/docs/usage/config.md index f1b9d5a0c..09198dddc 100644 --- a/docs/docs/usage/config.md +++ b/docs/docs/usage/config.md @@ -44,6 +44,7 @@ See specific example [configurations for your provider](/docs/category/providers - **register** - (`{ [key: string]: value }`) headers to be passed during registration request. - **additionalHeaders** - (`{ [key: string]: value }`) _IOS_ you can specify additional headers to be passed for all authorize, refresh, and register requests. - **useNonce** - (`boolean`) (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers. To specify custom nonce, provide it in `additionalParameters` under the `nonce` key. +- **state** - to specify a custom state, provide it in `additionalParameters` under the `state` key. A random state is generated when the key is absent. Pass `null` to omit the parameter entirely, to support providers that do not echo it back. Only omit it if you understand the CSRF tradeoff, and note that a provider returning a state you did not send will still fail validation. - **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers. - **skipCodeExchange** - (`boolean`) (default: false) just return the authorization response, instead of automatically exchanging the authorization code. This is useful if this exchange needs to be done manually (not client-side) - **iosCustomBrowser** - (`string`) (default: undefined) _IOS_ override the used browser for authorization, used to open an external browser. If no value is provided, the `ASWebAuthenticationSession` or `SFSafariViewController` are used by the `AppAuth-iOS` library. diff --git a/packages/react-native-app-auth/index.d.ts b/packages/react-native-app-auth/index.d.ts index 7ff7ec62b..f7b3532f6 100644 --- a/packages/react-native-app-auth/index.d.ts +++ b/packages/react-native-app-auth/index.d.ts @@ -59,6 +59,8 @@ interface BuiltInParameters { display?: 'page' | 'popup' | 'touch' | 'wap'; login_prompt?: string; prompt?: 'consent' | 'login' | 'none' | 'select_account'; + /** Custom state. Pass null to omit the parameter, for providers that do not echo it back. */ + state?: string | null; } export type BaseAuthConfiguration = BaseConfiguration & { diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index c48163427..111879801 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -344,6 +344,16 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration [mutableDict removeObjectForKey:@"state"]; [mutableDict removeObjectForKey:@"nonce"]; + // A null from JS bridges to NSNull, which is truthy in ObjC, so a plain + // ternary would send NSNull as the state. Absent generates, null omits. + id requestedState = additionalParameters[@"state"]; + NSString *state = nil; + if (requestedState == nil) { + state = [[self class] generateState]; + } else if (![requestedState isKindOfClass:[NSNull class]]) { + state = requestedState; + } + // builds authentication request OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration @@ -353,7 +363,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration scope:[OIDScopeUtilities scopesWithArray:scopes] redirectURL:[NSURL URLWithString:redirectUrl] responseType:OIDResponseTypeCode - state: additionalParameters[@"state"] ? additionalParameters[@"state"] : [[self class] generateState] + state:state nonce:nonce codeVerifier:codeVerifier codeChallenge:codeChallenge