Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 client and v4 server compat #1382

Closed
jamesholcomb opened this issue Jun 2, 2019 · 18 comments
Closed

v3 client and v4 server compat #1382

jamesholcomb opened this issue Jun 2, 2019 · 18 comments

Comments

@jamesholcomb
Copy link

I have @feathers-v3 React Native (Android and iOS) client utilizing

  • Sockets via socket-io
  • OAuth2 with a custom Passport strategy and Verifier
  • JWT cookie sent in the OAuth2 response for subsequent client authentication

I see from #1045 that cookies are no longer supported. Assuming this is still the case, it sounds implausible to rollout a v4 server that would be backward compatible with v3 clients. Given the nature of native App Store apps (having to support multiple versions of the same app when users do not upgrade) what do you recommend here?

@jamesholcomb
Copy link
Author

To clarify a bit more, the cookie is only used to transmit the Feathers JWT during OAuth flow.

@KidkArolis
Copy link
Contributor

KidkArolis commented Jun 2, 2019

The cookie is still being used in the OAuth flow in V4. In fact you need to configure sessions (i.e. express-session) when using the new feathers-authentication-oauth: https://github.com/feathersjs/feathers/blob/master/packages/authentication-oauth/src/express.ts#L32. And sessions are backed by cookies usually.

@daffl
Copy link
Member

daffl commented Jun 2, 2019

You should be able to use the following middleware:

const handleCookie = (name = 'feathers-jwt', cookieOptions = {}) =>
  (req, res, next) => {
    const { method } = res.hook;
    const { access_token } = res.data;

    if (access_token) {
      if (method === 'create') {
        res.cookie(name, access_token, cookieOptions);
      } else {
        res.clearCookie(name);
      }
    }

    next();
  };

app.use('/authentication', new AuthenticationService(), handleCookie());

Let me know if this works, I'll add it to the migration guide as well.

@daffl
Copy link
Member

daffl commented Jun 2, 2019

The session is used for the internal oAuth flow. There is no cookie anymore to transport the access token to the client. It uses a redirect string that works cross domain and avoids many of the problems and expectations people were having when using a cookie. I believe the above middleware should allow for backwards compatibility.

@jamesholcomb
Copy link
Author

I tried your code @daffl, but handleCookie is never invoked. The OAuth flow with my provider is completing successfully, I can still see the #access_token in the query string at the callback url.

const {
  AuthenticationService,
  JWTStrategy
} = require("@feathersjs/authentication")
const {
  OAuthStrategy,
  express: oauth
} = require("@feathersjs/authentication-oauth")

class StravaStrategy extends OAuthStrategy {
  async getEntityData(profile) {
    const baseData = await super.getEntityData(profile)

    return {
      ...baseData,
      email: profile.email
    }
  }
}

const handleCookie = (name = "feathers-jwt", cookieOptions = {}) => (
  req,
  res,
  next
) => {
  const { method } = res.hook
  const { access_token } = res.data

  if (access_token) {
    if (method === "create") {
      res.cookie(name, access_token, cookieOptions)
    } else {
      res.clearCookie(name)
    }
  }

  next()
}

module.exports = function() {
  const app = this

  const service = new AuthenticationService(app)

  service.register("jwt", new JWTStrategy())
  service.register("strava", new StravaStrategy())

  app.use("/authentication", service, handleCookie())
  app.configure(oauth())
}
  "authentication": {
    "secret": "FEATHERS_AUTH_SECRET",
    "strategies": ["jwt"],
    "path": "/authentication",
    "service": "users",
    "entity": "user",
    "jwtOptions": {
      "header": { "typ": "access" },
      "algorithm": "HS256"
    },
    "oauth": {
      "redirect": "/",
      "defaults": {
        "protocol": "http",
        "host": "localhost:3030",
        "transport": "session"
      },
      "strava": {
        "key": "STRAVA_CLIENT_ID",
        "secret": "STRAVA_CLIENT_SECRET",
        "scope": ["read", "profile:read_all"]
      }
    }
  }

@KidkArolis
Copy link
Contributor

Probably you need to make this change?

-const { access_token } = res.data
+const { access_token } = res.query

@KidkArolis
Copy link
Contributor

Unless, like you said.. it's not getting called at all?

@KidkArolis
Copy link
Contributor

KidkArolis commented Jun 3, 2019

Sorry, both my previous comments were unhelpful.

Another go.. @daffl the middleware wouldn't be called because, the OAuth Strategy itself does the redirect with no way to intercept it? The authentication.create call is an internal call in this case. https://github.com/feathersjs/feathers/blob/master/packages/authentication-oauth/src/express.ts#L56-L64

I'm not sure how you'd drop a cookie before doing this redirect. I had a similar issue where I was using cookies before, but with v4 you no longer have access to req/res in the oauth flow. What about changing getRedirect(data) to getRedirect(data, req, res)?

Apologies if I'm talking about the wrong thing yet again.

@KidkArolis
Copy link
Contributor

Ah, doing getRedirect(data, req, res) is bit of a leaky abstraction.

What you can do instead already is customize the OAuthStrategy’s getRedirect to redirect to /some/path?token=x. Then create a route handler for /some/path. In there, set the cookie and redirect to the final destination. I can write an example snippet of code when Im at my keyboard.

@daffl
Copy link
Member

daffl commented Jun 3, 2019

Sorry, my bad, this is indeed not called that way. This could be handled with redirects but I'm wondering if it makes sense to allow customizing the response middleware to do this.

@daffl
Copy link
Member

daffl commented Jun 4, 2019

Actually, thinking about it more, we might make it customizable later but you don't really get anything more than with a redirect. I got the cookie set successfully by updating src/authentication.js to the following:

module.exports = app => {
  const authService = new AuthenticationService(app);

  authService.register('jwt', new JWTStrategy());
  authService.register('local', new LocalStrategy());
  authService.register('github', new GitHubStrategy());

  app.use('/authentication', authService);
  app.get('/oauth/cookie', (req, res) => {
    const { access_token } = req.query;

    if (access_token) {
      res.cookie('feathers-jwt', access_token, {
        httpOnly: false
      });
    }

    res.redirect('/redirect-url');
  });

  app.configure(expressOauth());
};

Then setting "redirect": "/oauth/cookie?" in the configuration.

@jamesholcomb
Copy link
Author

Can you include your complete authentication json config section? I need to configure everything at path /auth as well (to preserve v3 compat).

@daffl
Copy link
Member

daffl commented Jun 4, 2019

Ah yes, sorry, it should be similar to

  "authentication": {
    "secret": "FEATHERS_AUTH_SECRET",
    "authStrategies": ["jwt"],
    "service": "users",
    "entity": "user",
    "jwtOptions": {
      "header": { "typ": "access" },
      "algorithm": "HS256"
    },
    "oauth": {
      "redirect": "/auth/cookie?",
      "defaults": {
        "path": "/auth",
        "protocol": "http",
        "host": "localhost:3030"
      },
      "strava": {
        "key": "STRAVA_CLIENT_ID",
        "secret": "STRAVA_CLIENT_SECRET",
        "scope": ["read", "profile:read_all"]
      }
    }
  }

@jamesholcomb
Copy link
Author

Looks good...had to step away from the upgrade for a bit. Closed and will re-open if I find any issues.

@jamesholcomb
Copy link
Author

I've migrated to feathers@4.5.1.

When I enter http://localhost:3030/auth/strava in a new private browser window (to ensure no cookies come along), then execute Strava OAuth flow successfully, I am redirected to:

http://localhost:3030/auth/connect/success?

with error:

error=Grant%3A%20missing%20or%20misconfigured%20provider

The logs def show that a JWT was minted:

@feathersjs/authentication/service Creating JWT with { userId: 5e7c0b11adda6f567dfbbc77 } { subject: '5e7c0b11adda6f567dfbbc77' } +1ms
@feathersjs/transport-commons/channels Publishing event created authentication +5ms
@feathersjs/authentication-oauth/express Successful oAuth authentication, sending response +229ms
express:router dispatching GET /auth/cookie?access_token=eyJhbGciOiJIUzI1..ZgGU8sCtuQxeg4 +236ms

The good news is I'm able to successfully auth with a React Native client (Feathers 4) and sockets at http://localhost:3030/auth/strava?redirect=myapp://auth when adding an async getRedirect(authResult, params) impl and using the params.redirect.

Note: The getRedirect impl was removed to reproduce the Grant error above. I only added it to get RN sockets working. Maybe I'm missing something?

{
  "host": "localhost",
  "port": "3030",
  "db": {
    "url": "mongodb://localhost:27017/oauth-test"
  },
  "redis": {
    "url": "redis://localhost:6379"
  },
  "log": {
    "console": {
      "level": "debug",
      "stderrLevels": ["error"],
      "colorize": true,
      "prettyPrint": true
    }
  },
  "authentication": {
    "secret": "secret",
    "authStrategies": ["jwt"],
    "path": "/authentication",
    "service": "users",
    "entity": "user",
    "jwtOptions": {
      "header": { "typ": "access" },
      "issuer": "feathers",
      "algorithm": "HS256",
      "audience": "https://yourdomain.com",
      "expiresIn": "1y"
    },
    "oauth": {
      "redirect": "/auth/cookie?",
      "defaults": {
        "path": "/auth"
      },
      "strava": {
        "key": "STRAVA_CLIENT_ID",
        "secret": "STRAVA_CLIENT_SECRET",
        "scope": ["read"]
      }
    }
  }
}

authentication.js

const querystring = require ("querystring")
const {
  AuthenticationService,
  JWTStrategy
} = require("@feathersjs/authentication")
const {
  OAuthStrategy,
  expressOauth
} = require("@feathersjs/authentication-oauth")

const v3AuthSuccessUri = "/auth/success"
const v3AuthErrorUri = "/auth/error"

class StravaStrategy extends OAuthStrategy {
  async getProfile(authResult) {
    const profile = await super.getProfile(authResult)

    return {
      ...profile,
      accessToken: authResult.access_token,
      refreshToken: authResult.refresh_token
    }
  }

  async getEntityData(profile, existing, params) {
    const baseData = await super.getEntityData(profile, existing, params)

    if (!baseData || !profile) {
      throw new Error(`Strava profile not found`)
    }

    const strava = {
      ...profile,
      oauthAt: new Date()
    }

    return {
      ...baseData,
      strava
    }
  }
}

class LegacyAuthenticationService extends AuthenticationService {
  async getPayload(authResult, params) {
    const payload = await super.getPayload(authResult, params)
    const { user } = authResult

    return {
      ...payload,
      userId: user._id
    }
  }
}

class LegacyJWTStrategy extends JWTStrategy {
  getEntityId(authResult) {
    const {
      authentication: { payload }
    } = authResult

    return payload.userId || payload.sub
  }
}

module.exports = function() {
  const app = this
  const service = new LegacyAuthenticationService(app)

  service.register("jwt", new LegacyJWTStrategy())
  service.register("strava", new StravaStrategy())

  app.use("/authentication", service)

  // v3 legacy clients
  app.get("/auth/cookie", (req, res) => {
    const { access_token } = req.query // eslint-disable-line camelcase

    // eslint-disable-next-line camelcase
    if (access_token) {
      res.cookie("feathers-jwt", access_token, {
        httpOnly: false,
        secure: false
      })
    }

    res.redirect(v3AuthSuccessUri)
  })

  app.configure(expressOauth())
}

@jamesholcomb jamesholcomb reopened this Mar 26, 2020
@primozs
Copy link

primozs commented Jan 4, 2021

Actually, thinking about it more, we might make it customizable later but you don't really get anything more than with a redirect. I got the cookie set successfully by updating src/authentication.js to the following:

module.exports = app => {
  const authService = new AuthenticationService(app);

  authService.register('jwt', new JWTStrategy());
  authService.register('local', new LocalStrategy());
  authService.register('github', new GitHubStrategy());

  app.use('/authentication', authService);
  app.get('/oauth/cookie', (req, res) => {
    const { access_token } = req.query;

    if (access_token) {
      res.cookie('feathers-jwt', access_token, {
        httpOnly: false
      });
    }

    res.redirect('/redirect-url');
  });

  app.configure(expressOauth());
};

Then setting "redirect": "/oauth/cookie?" in the configuration.

Hello @daffl

I have seen this
https://docs.feathersjs.com/guides/migrating.html#oauth-cookies
#1340

I am trying to connect feathers with nextjs and support ssr and client render. Because hash url with token is not getting to the server I need to transfer JWT to the app with cookie.

feather.api.auth.app.com to app.com

Will I run into some issues later with cookie transfer in production and because this cookie can not be httpOnly is this ok long term auth approach?

When service-worker takes over it ideally becomes client app. But if that is not the case, then cookie should not be removed on first auth redirect to be stored in some other storage on the client because of subsequent page reloads.

Any other alternatives except forgetting ssr?
On the static nextjs pages it would only work on the client side but store could still work with cookie.
Cheers

@daffl
Copy link
Member

daffl commented Jan 4, 2021

@primozs This is described in the Authentication section of the Server side rendering Cookbook entry.

@jamesholcomb I think I'm going to close this issue since it's been a while and I am not sure if someone can help without a full repository to reproduce with the latest versions (only thing I can think of is that the React Native does not (and really shouldn't) use any cookies).

@daffl daffl closed this as completed Jan 4, 2021
@primozs
Copy link

primozs commented Jan 4, 2021

Thx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants