passport icon indicating copy to clipboard operation
passport copied to clipboard

Added the ability for the client to specify their own redirect.

Open Panoplos opened this issue 8 years ago • 8 comments
trafficstars

This was badly needed for systems that require interfacing with the server via fetch or XHR--due to non-cookie based session, for instance--, which would result in CORS errors, per the OAuth2 specification.

One would use it in this case like:

app.use(customSession())
...
router.get('/auth/login', async (ctx, next) => {
  const service = ctx.query.service
  const accountType = ctx.query.type
  const sessionStore = await ctx.session.store()
  await sessionStore.set('authStrategy', service)
  await sessionStore.set('accountType', accountType)
  const redirect = (res, url, status) => {
    res.statusCode = 200
    res.end(url)
  }
  await passport.authenticate(service, { session: false, redirect })(ctx, next)
})
...

Then the client would use the service in this manner:

export const socialLoginLogic = createLogic({
  type: SOCIAL_LOGIN,
  latest: true,

  process({ getState, action }) {
    const { service, accountType } = action.payload
    const path = `/auth/login?service=${service}&type=${accountType}`
    fetch(path, {
      method: 'GET',
      headers: {
        [JWT_SESSION_HEADER]: getState()['session'].jwt
      }
    }).then( res => {
      if (res.status !== 200) {
        console.error(`${path} fetch failed with status ${res.status}`)
        throw new Error(`Social login failed with status code ${res.status}`)
      }
      return res.text()
    }).then( url => {
      window.location.replace(url)
    }).catch( err => {
      console.error(`${path} fetch failed with error:`, err)
    })
  }
})

And most importantly: Zero impact on existing systems.

Panoplos avatar Aug 08 '17 09:08 Panoplos

Coverage Status

Coverage decreased (-0.6%) to 98.319% when pulling 9a23caf88295b2b379e8b3a128fc7ea5f17fa10e on Panoplos:redirect-option into e458838634e306433d9b049a59e3e266fd5b889e on jaredhanson:master.

coveralls avatar Aug 08 '17 09:08 coveralls

Why would the client involve the server at all in the case, vs just doing OAuth fully client side using the implicit flow?

jaredhanson avatar Aug 09 '17 04:08 jaredhanson

The only difference between this code and the code that relies on redirects from the server is that the client proxies the redirect to avoid CORS. Using this method, I can add support for more services by simply adding passport plugins--which is the attractiveness of this library. If I could get away with calling into passport with fetch or XHR, I would, but CORS restrictions on OAuth2 prohibit it. I cannot simply use window.location to call the backend to kick this off, either, as I need to pass headers for the session (cookies are not a good solution because the app is native, too).

Panoplos avatar Aug 09 '17 07:08 Panoplos

If I could get away with calling into passport with fetch or XHR, I would, but CORS restrictions on OAuth2 prohibit it.

Could you provide more details on how the requests are made cross-origin, and what the resulting responses are. For example, with the example code you supply, the response would be a text URL (and assuming an OAuth-based strategy, might be seomthing like: https://myapp.com/login/callback?state=xvzfeef?

This state needs to be bound to the server-side session, and verified by the server side when the callback occurs. If you are calling this via XHR (or a native app), how is state verification occuring? There's also security concerns around exposing state values to any scripts that may be executing in a client-side browser.

Could you provide more details about the security considerations of this approach? Thanks.

jaredhanson avatar Aug 09 '17 17:08 jaredhanson

If I change the client call above to:

export const socialLoginLogic = createLogic({
  type: SOCIAL_LOGIN,
  latest: true,

  process({ getState, action }) {
    const { service, accountType } = action.payload
    const path = `/auth/login?service=${service}&type=${accountType}`
    fetch(path, {
      method: 'GET',
      headers: {
        [JWT_SESSION_HEADER]: getState()['session'].jwt
      }
    })
  }
})

And remove this patch on the backend, so it now looks like:

app.use(customSession())
...
router.get('/auth/login', async (ctx, next) => {
  const service = ctx.query.service
  const accountType = ctx.query.type
  const sessionStore = await ctx.session.store()
  await sessionStore.set('authStrategy', service)
  await sessionStore.set('accountType', accountType)
  await passport.authenticate(service, { session: false })(ctx, next)
})
...

It will result in:

passport cors error

The callback URI is the same as the domain that made that redirect: https://<mydomain>:3000/profile/step-two -- of course <mydomain> is the real domain name.

The flow using fetch or XHR is simple:

  1. Call backend with session-specific data (in this case, the social service being used and the client type).
  2. Server responds with the redirect URL. (This is not sensitive information, as any browser tools can pick it out.)
  3. Client pushes the URL.
  4. OAuth2 service verifies app and redirects to the callback URI.
  5. The Client intercepts the callback, extracts the code, then uses fetch or an XHR to finish the login by calling the backend with the code as a query parameter and the session header.

I do not see anything in this process that is compromising, and I would rather avoid pulling any passport logic other than simple redirection into the client.

I noticed another pull request that munges the redirect. His use case could also be handled by this. ;^)

Panoplos avatar Aug 10 '17 03:08 Panoplos

What would happen if you changed the client call to:

export const socialLoginLogic = createLogic({
  type: SOCIAL_LOGIN,
  latest: true,

  process({ getState, action }) {
    const { service, accountType } = action.payload
    const session = getState()['session'].jwt
    const path = `/auth/login?service=${service}&type=${accountType}&session=${session}`
    window.location.replace('https://mydomain.com/' + path)
  }
})

Or (perhaps even better) leave the session out of the request URL and just use the normal Cookie mechanism built into the browser?

I'm having a hard time understanding the rationale behind what you are attempting to do here. It seems you are going through a lot of trouble to simply create the equivalent of a Cookie header, but with a non-standard name.

jaredhanson avatar Aug 10 '17 21:08 jaredhanson

The rationale is that cookies are a cludge in React Native apps. I could put the session token in the URL, but this also does not allow for clean and easy handling of potential server-side error responses in the client context.

Panoplos avatar Aug 16 '17 05:08 Panoplos

How could I use this to solve my cors issue

george-abadier avatar Jan 28 '23 11:01 george-abadier