Closed jamesholcomb closed 3 years ago
To clarify a bit more, the cookie is only used to transmit the Feathers JWT during OAuth flow.
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.
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.
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.
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"]
}
}
}
Probably you need to make this change?
-const { access_token } = res.data
+const { access_token } = res.query
Unless, like you said.. it's not getting called at all?
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.
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.
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.
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.
Can you include your complete authentication
json config section? I need to configure everything at path /auth
as well (to preserve v3 compat).
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"]
}
}
}
Looks good...had to step away from the upgrade for a bit. Closed and will re-open if I find any issues.
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())
}
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 https://github.com/feathersjs/feathers/issues/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
@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).
Thx
I have @feathers-v3 React Native (Android and iOS) client utilizing
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?