launchdarkly / react-client-sdk

LaunchDarkly Client-side SDK for React.js
Other
86 stars 68 forks source link

Impossible to initialize SDK when using Secure Mode #116

Open qidydl opened 2 years ago

qidydl commented 2 years ago

Describe the bug We are building an application dealing with both financial and health information, so Secure Mode is a pretty strong requirement for us. However, if it is enabled, you cannot initialize the SDK to start with when calling withLDProvider (because the user hasn't logged in yet, we don't have user information or a hash), and you can't defer initialization and identify the user later because there's no LDClient instance returned from useLDClient to call identify on!

To reproduce Enable Secure Mode, and try to use the React Client SDK.

Expected behavior useLDClient always returns an LDClient so we can call identify after login has completed and finish initialization.

SDK version 2.25.0, latest as of this report

Language version, developer tools node.js 16.13.0 React 17.0.2

OS/platform Windows 10, Chrome

Additional context This may have sort of been reported already in comments on #67 but the developer there apparently just fixed the problem by turning secure mode off, which is not a viable option for us.

bwoskow-ld commented 2 years ago

Hi @qidydl,

Take a look at the Configuring secure mode in the JavaScript client-side SDK section at the very bottom of our secure mode feature documentation. There we document that this feature only works with predefined user keys -- as opposed to the automatically generated user keys for users like { anonymous: true }. This is because of how the SDK hashes the user key while running secure mode.

Aside from being unable to use withLDProvider without predefined user keys, you should otherwise be able to use withLDProvider with secure mode. You can provide a hash when first calling withLDProvider with a config where options has a hash attribute. You can then use the useLDClient hook to access the client and call identify on it, or alternatively provide an ldClient in your config if you wish to initialize the client outside of withLDProvider.

Cheers, @bwoskow-ld

qidydl commented 2 years ago

I understand the restriction for auto-generated keys, but we're not trying to make use of that. We cannot provide a hash when first calling withLDProvider, because that has to occur before our app even renders (much less before we've had an opportunity to contact the back-end and obtain a hash), because it has to be wrapped around our app component as a HOC (which is totally unlike how all the other context-providing libraries that we use work, and forced some changes I would have preferred not to make). There doesn't seem to be any good way to finish initializing after it has been deferred, as if the deferring part got implemented and there was no consideration to how to recover afterwards. Every article I've managed to find takes shortcuts like hard-coding the user details or ignores secure mode entirely. At the moment the only viable option appears to be to give up on the React SDK and drop down to the underlying JavaScript SDK and manage the client ourselves in our own context, so we can actually initialize it.

bwoskow-ld commented 2 years ago

I understand your problem, but the hash has to be provided as part of the SDK configuration options at initialization time, and there's no way to use the SDK prior to initializing it. The SDK allows for deferring the user initialization but not the SDK configuration as that's fundamental for how the SDK operates.

eli-darkly commented 2 years ago

much less before we've had an opportunity to contact the back-end and obtain a hash

If the page is rendered by the back end, rather than being a static file, then there would be no need to "contact the back-end". There could be a hard-coded initial user key that you use for the "not logged in yet" state, and the hash for that key could be embedded in the page already.

qidydl commented 2 years ago

The JavaScript SDK LDClient could decouple initialization from instantiation so that it waits until it is provided with user information and a hash to try contacting the server and avoid failures due to secure mode, but that isn't actually necessary to fix this. The React SDK already has a "defer initialization" mode that leaves the context in a blank state without creating the LDClient until later, there just needs to be a hook or some sort of property exposed on the context that allows us to fill in the user details and hash.

The page being rendered by the back end is not a viable solution; we're following what is nowadays a common and effective pattern of a React SPA served out of S3 with a back end that is just an API. Furthermore, that doesn't work for login pages or anything served up before the user has logged in.

eli-darkly commented 2 years ago

@qidydl:

The page being rendered by the back end is not a viable solution; we're following what is nowadays a common and effective pattern of a React SPA served out of S3 with a back end that is just an API. Furthermore, that doesn't work for login pages or anything served up before the user has logged in.

My comment certainly may not have been applicable to your situation— I mentioned it only as an example of how some other customers have handled a similar situation.

But, to be clear, I was not suggesting that all of the content you're currently rendering on the front end should be rendered on the back end instead. My comment only referred to embedding a single value— a hash code corresponding to some hard-coded user key that would represent the not-logged-in state— somewhere in the page where it could be accessed by front-end logic. Whether the page is "served up before the user has logged in" would not be relevant, since the point of this hard-coded user key would be to use it for initializing the SDK before you know who the actual user is.

Again, this may not be feasible for you, but I wanted to clarify what I was talking about since others may be following this discussion as well.

eli-darkly commented 2 years ago

And unfortunately I can't comment on the React SDK deferred initialization issue in general because I'm not a React developer; I maintain the JS SDK. I will have to leave that part to others.

zachbwh commented 2 years ago

Would be super interested to know if you came to any resolution here! We're having the exact same issues. @eli-darkly would you mind tagging someone who works on the React SDK?

qidydl commented 2 years ago

Our solution, which worked out fine, is to just use the plain JavaScript SDK and integrate with it manually. We already had a React context containing authentication state for our application, so we made that context hold and interact with the LDClient and we just don't initialize it until we've logged in. We then had to expose the LDClient (or the data we use from it) through our context, so there's some rework there, but it's not a big deal. We'll probably just leave that and never bother with the React SDK at this point.

zachbwh commented 2 years ago

Yeah 100% fair enough! thanks for replying so quick btw.

We do have a solution that allows some serverside variable injection but it introduces coupling that we're trying to avoid going into the future. Not a hard blocker at the moment, but feels like you should definitely be able to do what we're trying to do with the React SDK.

Hopefully by the time we need a 100% clientside React solution, this issue will be addressed

yusinto commented 2 years ago

there just needs to be a hook or some sort of property exposed on the context that allows us to fill in the user details and hash

Hi @qidydl , the React SDK exports the LDProvider component which you can use as an alternative way to initialize the ldClient. The example below defers initialization and accepts a user and hash prop. These props let you control the initialization of the ldClient when the user and hash become available. Will this work for you?

import { LDProvider } from 'launchdarkly-react-client-sdk';
import { LDUser } from 'launchdarkly-js-client-sdk';

const App = (user?: LDUser, hash?: string) => (
  <LDProvider clientSideID="your-client-side-id" options={{ hash }} user={user} deferInitialization>
    <main>Application content here</main>
  </LDProvider>
);