404 sent to client (browser) during google callback, but the user data is retrieved.
Panoplos opened this issue · 7 comments
When calling passport.authenticate(...)
in the OAuth2 callback stage of the authentication sequence for Google OAuth2 as follows:
const GoogleStrategy = require('passport-google-oauth2').Strategy
passport.use(new GoogleStrategy({
scope: ['email', 'profile'],
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:' + 7000 + '/auth/google/callback'
},
(accessToken, refreshToken, profile, done) => {
console.log(profile)
fetchUser().then(user => done(null, user))
}
))
app.use(route.get('/auth/google', async (ctx, next) => {
console.log(`/api/google/singin called with\n ctx -> ${JSON.stringify(ctx)} && next -> ${next}`)
console.log(`ctx.req.headers -> ${JSON.stringify(ctx.req.headers)}`)
console.log(`ctx.req.rawHeaders -> ${ctx.req.rawHeaders}`)
passport.authenticate('google')(ctx,next)
}
))
app.use(route.get('/auth/google/callback', async (ctx, next) => {
passport.authenticate('google', async (err, user, info) => {
console.log(`/auth/google/signin/callback -> passport.authenticate('google') callback called with\nerr -> ${JSON.stringify(err)}\nuser -> ${JSON.stringify(user)}\ninfo -> ${JSON.stringify(info)}`)
if (user === false) {
ctx.redirect('/')
await next()
} else {
ctx.login(user)
ctx.redirect('/app')
await next()
}
})(ctx, next)
}))
Koa is sending 404 headers to the client (browser),
but the anonymous function callback is called with the actual user data.
The debug output shows the sequence of calls (removed personal info):
<-- GET /favicon.ico
--> GET /favicon.ico 302 6ms 33b
<-- GET /
--> GET / 200 5ms 719b
<-- GET /auth/google
/api/google/singin called with
ctx -> {"request":{"method":"GET","url":"/auth/google","header":{"host":"localhost:7000","connection":"keep-alive","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","dnt":"1","referer":"http://localhost:7000/","accept-encoding":"gzip, deflate, sdch, br","accept-language":"en-US,en;q=0.8,ja;q=0.6","cookie":"koa.sid.sig=O9oTTBivYar3gy7JuZtdpDgvTO0"}},"response":{"status":404,"message":"Not Found","header":{}},"app":{"subdomainOffset":2,"proxy":true,"env":"development"},"originalUrl":"/auth/google","req":"<original node req>","res":"<original node res>","socket":"<original node socket>"} && next -> function next() {
return dispatch(i + 1)
}
ctx.req.headers -> {"host":"localhost:7000","connection":"keep-alive","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","dnt":"1","referer":"http://localhost:7000/","accept-encoding":"gzip, deflate, sdch, br","accept-language":"en-US,en;q=0.8,ja;q=0.6","cookie":"koa.sid.sig=O9oTTBivYar3gy7JuZtdpDgvTO0"}
ctx.req.rawHeaders -> Host,localhost:7000,Connection,keep-alive,Upgrade-Insecure-Requests,1,User-Agent,Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36,Accept,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8,DNT,1,Referer,http://localhost:7000/,Accept-Encoding,gzip, deflate, sdch, br,Accept-Language,en-US,en;q=0.8,ja;q=0.6,Cookie,koa.sid.sig=O9oTTBivYar3gy7JuZtdpDgvTO0
--> GET /auth/google 302 4ms 0b
<-- GET /auth/google/callback?code=4/vE4A-Yl3nZS07-x61cgapHEf2UhX3b-ptn4K0wdSLv0
--> GET /auth/google/callback?code=4/vE4A-Yl3nZS07-x61cgapHEf2UhX3b-ptn4K0wdSLv0 404 9ms -
<-- GET /favicon.ico
--> GET /favicon.ico 302 2ms 33b
<-- GET /
--> GET / 200 22ms 719b
{ provider: 'google',
id: 'XXXXXXXXXXXXXXX',
displayName: XXXXXXXXXXXXXXX',
name: { familyName: 'XXXXXXXXXXXXXXX', givenName: 'XXXXXXXXXXXXXXX' },
isPerson: true,
isPlusUser: true,
language: 'en',
emails: [ { value: 'XXXXXXXXXXXXXXX', type: 'account' } ],
email: 'XXXXXXXXXXXXXXX',
gender: 'XXXXXXXXXXXXXXX',
photos: [ { value: 'https://lh5.googleusercontent.com/XXXXXXXXXXX/photo.jpg?sz=50' } ],
_raw: '{\n "kind": "plus#person",\n "etag": "\\"XXXXXXXXXXXXXX\\"",\n "gender": "XXXXXXXXX",\n "emails": [\n {\n "value": "XXXXXXXXXXXXXXX",\n "type": "account"\n }\n ],\n "objectType": "person",\n "id": "XXXXXXXXXXXXXXX",\n "displayName": "XXXXXXXXXXXXXXX",\n "name": {\n "familyName": "XXXXXXXXXXXXXXX",\n "givenName": "XXXXXXXXXXXXXXX"\n },\n "url": "https://plus.google.com/XXXXXXXXXXXXXXX",\n "image": {\n "url": "https://lh5.googleusercontent.com/XXXXXXXXXXXXXXX/photo.jpg?sz=50",\n "isDefault": false\n },\n "isPlusUser": true,\n "language": "en",\n "circledByCount": 0,\n "verified": false\n}\n',
_json:
{ kind: 'plus#person',
etag: '"XXXXXXXXXXXXXXX"',
gender: 'XXXXXXXXXXXXXXX',
emails: [ [Object] ],
objectType: 'person',
id: 'XXXXXXXXXXXXXXX',
displayName: 'XXXXXXXXXXXXXXX',
name: { familyName: 'XXXXXXXXXXXXXXX', givenName: 'XXXXXXXXXXXXXXX' },
url: 'https://plus.google.com/XXXXXXXXXXXXXXX',
image:
{ url: 'https://lh5.googleusercontent.com/XXXXXXXXXXXXXXX/photo.jpg?sz=50',
isDefault: false },
isPlusUser: true,
language: 'en',
circledByCount: 0,
verified: false } }
/auth/google/signin/callback -> passport.authenticate('google') callback called with
err -> null
user -> {"id":1,"username":"test","password":"test"}
info -> {}
(node:54360) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 4): Error: Can't set headers after they are sent.
According to your logs, you get the following error: UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 4): Error: Can't set headers after they are sent.
This could be because you do a redirect ctx.redirect('/app')
but still continue in the middleware chain afterwards with await next()
.
Furthermore, it I think should be await ctx.login(user)
, but this is something I forgot to mention in the README (will be updated).
Sorry to say that this is not the problem, I still get the 404 and "can't set headers after they are sent" message, even with the following code:
app.use(route.get('/auth/google/callback',
/*passport.authenticate('google', { failureRedirect: '/', successRedirect: '/app' })))*/
async (ctx, next) => {
console.log(`/auth/google/singin called with\n ctx -> ${JSON.stringify(ctx)} && next -> ${next}`)
console.log(`ctx.req.headers -> ${JSON.stringify(ctx.req.headers)}`)
console.log(`ctx.req.rawHeaders -> ${ctx.req.rawHeaders}`)
passport.authenticate('google', async (err, user, info) => {
console.log(`/auth/google/signin/callback -> passport.authenticate('google') callback called with\nerr -> ${JSON.stringify(err)}\nuser -> ${JSON.stringify(user)}\ninfo -> ${JSON.stringify(info)}`)
if (user === false) {
ctx.redirect('/')
} else {
await ctx.login(user)
ctx.redirect('/app')
}
})(ctx)
}))
I have been looking at the core passport stuff, and there seems to be a race condition where Application.handleResponse -> respond(ctx)
is being called with a 404 (default) and empty body after processing the google callback but before the "verify" action occurs in the strategy. I have managed to get around this by setting ctx.respond = false
before making the call to authenticate
, but this is not a fix as now the session is not getting saved correctly after the user is serialised, and is getting reset on the next request (for "/app").
I missed the following when first having a look, I think you have to call await passport.authenticate()
when calling it manually
And we have liftoff!!
For documentation purposes, here is the code that works:
app.use(route.get('/auth/google/callback', async (ctx, next) => {
await passport.authenticate('google', async (err, user, info) => {
if (user === false) {
ctx.redirect('/')
} else {
await ctx.login(user)
ctx.redirect('/app')
}
})(ctx)
}))
Awesome!
I think the code could be simplified into into:
app.use(route.get('/auth/google/callback', passport.authenticate('google', async (err, user, info) => {
if (user === false) {
ctx.redirect('/')
} else {
await ctx.login(user)
ctx.redirect('/app')
}
})))
It is sent to the function returned by passport.authenticate(...)
.