superfaceai / passport-twitter-oauth2

Twitter OAuth 2.0 Strategy for Passport for accessing Twitter API v2
MIT License
29 stars 9 forks source link

TokenError: Missing valid authorization header #42

Open cSarcasme opened 1 year ago

cSarcasme commented 1 year ago

I have search a lot a solution for my issue but i dont find it!

you are my last hope :) !

error i have when i connect with twitter i arrive to the twitter page and give the authorization on my twitter account after that i have this error:

TokenError: Missing valid authorization header at OAuth2Strategy.parseErrorResponse (C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\passport-oauth2\lib\strategy.js:373:12) at OAuth2Strategy._createOAuthError (C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\passport-oauth2\lib\strategy.js:420:16) at C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\passport-oauth2\lib\strategy.js:177:45 at C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\oauth\lib\oauth2.js:191:18 at passBackControl (C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\oauth\lib\oauth2.js:132:9) at IncomingMessage. (C:\Users\haatman\Desktop\dbq2formydev-main\dbq2formydev-main\bck\node_modules\oauth\lib\oauth2.js:157:7) at IncomingMessage.emit (node:events:526:35) at endReadableNT (node:internal/streams/readable:1359:12) at process.processTicksAndRejections (node:internal/process/task_queues:82:21)

my code:

**middleware**
    require('dotenv').config();
const passport = require('passport');
const Strategy = require('@superfaceai/passport-twitter-oauth2').Strategy;
const TwitterUser = require('../models/TwitterUser');
const jwt = require('jsonwebtoken');

passport.serializeUser((user, done) => {
    if (user.provider === 'twitter') {
        //console.log('Twitter-userSerialize', user);
        //console.log('Twitter-userID', user.id);
        done(null, { type: 'twitterUser', idTwitter: user.id });
    } else {
        // L'utilisateur ne provient pas de Twitter, ne rien faire
        done();
    }
});

// Désérialise l'utilisateur à partir de la session
passport.deserializeUser(async (serializedUser, done) => {
    if (serializedUser.type === 'twitterUser') {
        try {
            let user = await TwitterUser.findById(serializedUser.idTwitter);
            // console.log('twitterUser', user)
            done(null, user);
        } catch (error) {
            done(error, null);
        }
    } else {
        done();
    }
});

// Use the Twitter OAuth2 strategy within Passport
passport.use(
    // <2> Strategy initialization
    new Strategy(
        {
            clientID: process.env.TWITTER_CLIENT_ID,
            clientSecret: process.env.TWITTER_CLIENT_ID_SECRET,
            clientType: 'confidential',
            callbackURL: process.env.CALLBACK_TWITTER,
        },
        // <3> Verify callback
        async (accessToken, refreshToken, profile, done) => {
            //console.log(profile);

            try {
                const { id, _json } = profile;
                const name = _json.name;
                const screen_name = _json.screen_name;
                const followers_count = _json.followers_count;
                const findUser = await TwitterUser.findOne({ twitterId: id });

                if (findUser) {
                    console.log('access token', accessToken);
                    console.log('tokenSecret', refreshToken);
                    findUser.accessToken = jwt.sign(
                        { accessToken },
                        process.env.JWT_TWITTER_CLIENT_TOKEN
                    );
                    findUser.refreshToken = jwt.sign(
                        { refreshToken },
                        process.env.JWT_TWITTER_CLIENT_TOKEN_SECRET
                    );

                    await findUser.save();

                    return done(null, findUser);
                } else {
                    console.log('access token', accessToken);
                    console.log('tokenSecret', refreshToken);
                    //console.log('pofileTest',id,name, screen_name,followers_count)
                    const newUser = await TwitterUser.create({
                        twitterId: id,
                        userName: name,
                        screenName: screen_name,
                        followersCount: followers_count,
                        accessToken: jwt.sign(
                            { accessToken },
                            process.env.JWT_TWITTER_CLIENT_TOKEN
                        ),
                        refreshToken: jwt.sign(
                            { refreshToken },
                            process.env.JWT_TWITTER_CLIENT_TOKEN_SECRET
                        ),
                    });
                    return done(null, newUser);
                }
            } catch (error) {
                console.error(error);
                return done(error, null);
            }
        }
    )
);

module.exports = passport;

my roads:

require('dotenv').config();
const express = require('express');
const passport = require('passport');
const isAuthenticated = require('../../middleware/authorized');

const router = express.Router();

router.get(
    '/twitter',
    passport.authenticate('twitter', {
        scope: [
            'tweet.read',
            'users.read',
            'like.read',
            'follows.read',
            'space.read',
            'list.read',
            'offline.access',
        ],
    }),
    (req, res) => {
        res.status(200);
    }
);

router.get(
    '/twitter/redirect',
    passport.authenticate('twitter', {
        failureRedirect: 'http://localhost:3000/',
    }),
    (req, res) => {
        // Affectez les données de session à req.session.twitterSession
        res.redirect('http://localhost:3000/');
    }
);

router.get('/status', isAuthenticated, (req, res) => {
    //console.log(req);
    if (req.user) {
        res.status(200).send(req.user);
    } else {
        res.status(401).json({ message: 'Unauthorized' });
    }
});
module.exports = router;

express-session and save cookie into mongoose

// appTwitter.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const MongoStore = require('connect-mongo');
require('./middleware/twitter');

const twitterSession = express();

twitterSession.use(
    session({
        secret: process.env.SESSION_SECRET_TWITTER,
        resave: false,
        saveUninitialized: false,
        cookie: {
            maxAge: 6000 * 60 * 24 * 7,
        },
        store: MongoStore.create({
            mongoUrl:
                **********************************************************************************,
            collectionName: 'sessionTwitter',
        }),
        name: 'twitterConnect',
    })
);

twitterSession.use(passport.initialize());

twitterSession.use(passport.session());
// ... autres configurations spécifiques à Twitter

module.exports = twitterSession;

I hope someone can help me i ty by advance :)

If you need other information tell me.

cSarcasme commented 1 year ago

Anyone to help me please ?

I ty by advance.

janhalama commented 1 year ago

Hi, I will look at it today and give you some answers.

Jan

cSarcasme commented 1 year ago

thanks a lot mr janhalama i will wait

janhalama commented 1 year ago

From the call stack you provided it looks like that Twitter strategy is not configured properly. Specifically custom headers are not configured.

Custom headers are configured if the clientType option is set to confidential in the strategy constructor. From the code snippets you provided I can not say where exactly is the problem.

What is the version of @superfaceai/passport-twitter-oauth2 lib you have installed? Did you try the basic example app with your Twitter App configuration?

cSarcasme commented 1 year ago

ty for your quick answer i will try with the basic example and come back to you with the result and maybe it will help me find the solution.

Also i have that intalled "@superfaceai/passport-twitter-oauth2": "^1.2.3",

i think it i the last version ?

you also writte lib behind @superfaceai/passport-twitter-oauth2 may be you speak about other thing.

i will search also more information about custom header

i come back when i have all one that ty

janhalama commented 1 year ago

ty for your quick answer i will try with the basic example and come back to you with the result and maybe it will help me find the solution.

Or, if you can provide a minimum reproducible example, I can help.

Also i have that intalled "@superfaceai/passport-twitter-oauth2": "^1.2.3",> i think it i the last version ?

Yes it is the latest version, there was an update in the library recently, but as you are on the latest version it is not the case.

cSarcasme commented 1 year ago

ia am on it now i had to finish a other part of my code before to come back to that so i am on it i look little more on my side and if i dont find i will provide you a minimum reproductible sorry for the delay.

Now i don' t do other dev thing until i find the solution ;) haha ty for your time

cSarcasme commented 1 year ago

So i have try many things and if i put in the developer portal on native app public client it work(but i am not sure it is good to do that for security)

I have look inside the module and normally if it is in clientType: 'confidential' the module take the clientID and clientSecret and create himself the header i don t understand why it not work.

I have try myself to add the headers like that too and always the same message

passport.use(
    // <2> Strategy initialization
    new Strategy(
        {
            clientID: process.env.TWITTER_CLIENT_ID,
            clientSecret: process.env.TWITTER_CLIENT_ID_SECRET,
            clientType: 'confidential',
            callbackURL: process.env.CALLBACK_TWITTER,
            customHeaders: {
                Authorization:
                    'Basic ' +
                    Buffer.from(
                        `${process.env.TWITTER_CLIENT_ID}:${process.env.TWITTER_CLIENT_ID_SECRET}`
                    ).toString('base64'),
            },
        },
        // <3> Verify callback
        async (accessToken, refreshToken, profile, done) => {
            console.log('AccessToken:', accessToken);
            console.log('RefreshToken:', refreshToken);
            //console.log('profile', profile);
            try {
                const { id, _json } = profile;
                const name = _json.name;
                const userName = _json.username;

                const findUser = await TwitterUser.findOne({ twitterId: id });

                if (findUser) {
                    console.log('access token', accessToken);
                    console.log('tokenSecret', refreshToken);
                    findUser.accessToken = jwt.sign(
                        { accessToken },
                        process.env.JWT_TWITTER_CLIENT_TOKEN
                    );
                    findUser.refreshToken = jwt.sign(
                        { refreshToken },
                        process.env.JWT_TWITTER_CLIENT_TOKEN_SECRET
                    );

                    await findUser.save();

                    return done(null, findUser);
                } else {
                    console.log('access token', accessToken);
                    console.log('tokenSecret', refreshToken);
                    //console.log('pofileTest',id,name, screen_name,followers_count)
                    const newUser = await TwitterUser.create({
                        twitterId: id,
                        name: name,
                        username: userName,
                        accesstoken: jwt.sign(
                            { accessToken },
                            process.env.JWT_TWITTER_CLIENT_TOKEN
                        ),
                        refreshtoken: jwt.sign(
                            { refreshToken },
                            process.env.JWT_TWITTER_CLIENT_TOKEN_SECRET
                        ),
                    });
                    return done(null, newUser);
                }
            } catch (error) {
                console.error(error);
                return done(error, null);
            }
        }
    )
);

I have try to change customHeaders to headers and same issue.

I need to advance on my project (i have the automation quest to do link to twitter) so i will put on public in wait to find a solution but i really need to find it for the security before go to live.

if you see something i have no see tell me ty for your help

jfrader commented 9 months ago

@cSarcasme I had this issue and the problem was actually my Nginx proxy not forwarding the protocol and I needed to add

proxy_set_header X-Forwarded-Proto $scheme;

to my Nginx configuration

Another possibility is that your cookie is not being set correctly probably becuase of domain or some other settings.

Also if you are behind a proxy you need to trust proxy in express.

Olliebrown commented 3 months ago

I am experiencing this same problem and I am not making use of Nginx or any other proxy server, just using express directly.

Once twitter completes the authorization and redirects, it reports the following error:

TokenError: Missing valid authorization header
    at OAuth2Strategy.parseErrorResponse (E:\Programming-School\vesper-server\node_modules\passport-oauth2\lib\strategy.js:373:12)
    at OAuth2Strategy._createOAuthError (E:\Programming-School\vesper-server\node_modules\passport-oauth2\lib\strategy.js:420:16)
    at E:\Programming-School\vesper-server\node_modules\passport-oauth2\lib\strategy.js:177:45
    at E:\Programming-School\vesper-server\node_modules\oauth\lib\oauth2.js:196:18
    at passBackControl (E:\Programming-School\vesper-server\node_modules\oauth\lib\oauth2.js:132:9)
    at IncomingMessage.<anonymous> (E:\Programming-School\vesper-server\node_modules\oauth\lib\oauth2.js:157:7)
    at IncomingMessage.emit (node:events:531:35)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21)

I'm also using the google and Facebook authorization strategies with passport and those are working okay. Only this twitter OAuth2 one fails. I've tried to compress all of this into a minimal example but naturally, it's not very minimal (there's a lot of pieces to get this working). In any case, I'll attach it here. Any help or suggestions would be much appreciated!

// Core libraries
import fs from 'fs'
import https from 'https'

// Debug output libraries
import Debug from 'debug'
import dotenv from 'dotenv'

// Express and middleware libraries
import express from 'express'
import morganLog from 'morgan'
import cookieParser from 'cookie-parser'
import querystring from 'querystring'

// MongoDB session management
import session from 'express-session'
import connectMongoDB from 'connect-mongodb-session'

// MongoDB driver for user accounts
import { MongoClient, ServerApiVersion } from 'mongodb'

// Passport and various strategies
import passport from 'passport'
import { Strategy as GoogleStrategy } from 'passport-google-oidc'
import { Strategy as FacebookStrategy } from 'passport-facebook'
import { Strategy as XStrategy } from '@superfaceai/passport-twitter-oauth2'

// Read secrets and settings from .env file to local variables
dotenv.config()
const MONGO_SERVER = process.env.MONGO_SERVER ?? 'badServer.mongodb.net'
const MONGO_USER = process.env.MONGO_USER ?? 'badUser'
const MONGO_PASS = process.env.MONGO_PASS ?? 'badPass'
const SESSION_SECRET = process.env.SESSION_SECRET ?? 'keyboard cat'

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID ?? 'badGoogleId'
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET ?? 'badGoogleSecret'
const FACEBOOK_CLIENT_ID = process.env.FACEBOOK_CLIENT_ID ?? 'badFacebookId'
const FACEBOOK_CLIENT_SECRET = process.env.FACEBOOK_CLIENT_SECRET ?? 'badFacebookSecret'
const X_CLIENT_ID = process.env.X_CLIENT_ID ?? process.env.TWITTER_CLIENT_ID ?? 'badXKey'
const X_CLIENT_SECRET = process.env.X_CLIENT_SECRET ?? process.env.TWITTER_CLIENT_SECRET ?? 'badXSecret'

const LISTEN_PORT = process.env.DEV_PORT

// Create debug logger
const debug = Debug('vesper:server-minimal')

// Raw login page
const loginPage = fs.readFileSync('public/login.html', { encoding: 'utf8' })

// Build connection URI
const mongoDbURI = `mongodb+srv://${MONGO_USER}:${MONGO_PASS}@${MONGO_SERVER}/?retryWrites=true&w=majority`

// Create a MongoClient with a MongoClientOptions object to set the Stable API version
const client = new MongoClient(mongoDbURI, {
  serverApi: {
    version: ServerApiVersion.v1,
    strict: true,
    deprecationErrors: true
  }
})

// Lookup credentials for a given provider and providerId (might be null)
async function retrieveCredentials (provider, providerId) {
  try {
    const collection = client.db('Users').collection('FederatedCredentials')
    return await collection.findOne({ provider, providerId })
  } catch (error) {
    debug(error)
    return null
  }
}

// Lookup a user by their userId
async function retrieveUser (userId) {
  try {
    const collection = client.db('Users').collection('Users')
    return await collection.findOne({ _id: userId })
  } catch (error) {
    debug(error)
    return null
  }
}

// Create a new user entry in the Users collection
async function createUser (username) {
  try {
    const collection = client.db('Users').collection('Users')
    const result = await collection.insertOne({ username })
    return result.acknowledged ? result.insertedId : null
  } catch (error) {
    debug(error)
    return null
  }
}

// Create a new federated credential entry in the FederatedCredentials collection
async function createFederatedCredential (userId, provider, providerId) {
  try {
    const collection = client.db('Users').collection('FederatedCredentials')
    const result = await collection.insertOne({ userId, provider, providerId })
    return result.acknowledged ? result.insertedId : null
  } catch (error) {
    debug(error)
    return null
  }
}

// Just-In-Time provisioning of user credentials
async function jitProvision (provider, profile) {
  try {
    const credentials = await retrieveCredentials(provider, profile.id)
    if (!credentials) {
      const newUserId = await createUser(profile.displayName)
      await createFederatedCredential(newUserId, provider, profile.id)
      return { id: newUserId, name: profile.displayName }
    } else {
      const user = await retrieveUser(credentials.userId)
      return user
    }
  } catch (error) {
    debug(error)
    return null
  }
}

// Configure Passport Strategies

// Authorize via Google Credentials
passport.use(new GoogleStrategy({
  clientID: GOOGLE_CLIENT_ID,
  clientSecret: GOOGLE_CLIENT_SECRET,
  callbackURL: '/oauth2/redirect/google',
  scope: ['profile', 'email'],
  state: true
}, async (issuer, profile, cb) => {
  debug('Google profile:')
  debug(profile)

  const user = await jitProvision(issuer, profile)
  if (!user) { return cb(new Error('Failed to provision credentials')) }
  const cred = {
    providerId: profile?.emails?.[0]?.value ?? profile.id,
    provider: issuer,
    name: profile.displayName ?? undefined
  }

  return cb(null, user, { credential: cred })
}))

// Authorize via Facebook Credentials
passport.use(new FacebookStrategy({
  clientID: FACEBOOK_CLIENT_ID,
  clientSecret: FACEBOOK_CLIENT_SECRET,
  callbackURL: '/oauth2/redirect/facebook',
  scope: ['public_profile', 'email'],
  state: true
}, async (accessToken, refreshToken, profile, cb) => {
  debug('Meta profile:')
  debug(profile)

  const user = await jitProvision('https://www.facebook.com', profile)
  if (!user) { return cb(new Error('Failed to provision credentials')) }
  const cred = {
    providerId: profile?.emails?.[0]?.value ?? profile.id,
    provider: 'https://www.facebook.com',
    name: profile.displayName ?? undefined
  }

  return cb(null, user, { credential: cred })
}))

// Authorize via Twitter/X Credentials
passport.use(new XStrategy({
  clientID: X_CLIENT_ID,
  clientSecret: X_CLIENT_SECRET,
  clientType: 'confidential',
  callbackURL: '/oauth/callback/x',
  scope: ['tweet.read', 'users.read', 'offline.access'],
  state: true
}, async (accessToken, refreshToken, profile, cb) => {
  debug('Twitter profile:')
  debug(profile)

  const user = await jitProvision('https://x.com', profile)
  if (!user) { return cb(new Error('Failed to provision credentials')) }
  const cred = {
    providerId: profile?.emails?.[0]?.value ?? profile.username ?? profile.id,
    provider: 'https://x.com',
    name: profile.displayName ?? undefined
  }

  return cb(null, user, { credential: cred })
}))

// Other passport configuration
passport.serializeUser((user, cb) => {
  process.nextTick(() => {
    cb(null, { id: user.id, username: user.username, name: user.name })
  })
})

passport.deserializeUser((user, cb) => {
  process.nextTick(() => {
    return cb(null, user)
  })
})

function setFederatedCredentialCookie (req, res, next) {
  const credential = req.authInfo.credential
  if (!credential) { return next() }
  res.cookie('fc', querystring.stringify(credential))
  next()
}

// Initialize express server middleware
const app = express()
app.use(morganLog('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.static('public'))

// Set up session middleware
const MongoDBStore = connectMongoDB(session)
app.use(session({
  secret: SESSION_SECRET,
  resave: false, // don't save session if unmodified
  saveUninitialized: false, // don't create session until something stored
  store: new MongoDBStore({ uri: mongoDbURI, databaseName: 'Users', collection: 'Sessions' })
}))

// Install passport authentication
app.use(passport.authenticate('session'))

// Show the login page
app.get('/login', (req, res) => {
  res.setHeader('Content-Type', 'text/html')
  res.send(loginPage)
})

// Google login routes
app.get('/login/federated/google', passport.authenticate('google'))
app.get('/oauth2/redirect/google', passport.authenticate('google', {
  keepSessionInfo: true,
  failureRedirect: '/login'
}), setFederatedCredentialCookie, (req, res) => {
  debug('Google auth success')
  res.json(req.user)
})

// Facebook login routes
app.get('/login/federated/facebook', passport.authenticate('facebook'))
app.get('/oauth2/redirect/facebook', passport.authenticate('facebook', {
  keepSessionInfo: true,
  failureRedirect: '/login'
}), setFederatedCredentialCookie, (req, res) => {
  debug('Meta/Facebook auth success')
  res.json(req.user)
})

// Twitter login routes
app.get('/login/federated/x', passport.authenticate('twitter'))
app.get('/oauth/callback/x', passport.authenticate('twitter', {
  keepSessionInfo: true,
  failureRedirect: '/login'
}), setFederatedCredentialCookie, (req, res) => {
  debug('twitter/x auth success')
  res.json(req.user)
})

// Logout route
app.post('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) { return next(err) }
    debug('Logged out')
    res.json({ message: 'Logged out' })
  })
})

// Set express port
app.set('port', LISTEN_PORT)

// Create HTTPS server.
const SSLKey = fs.readFileSync('server/testingCert/key.pem')
const SSLCert = fs.readFileSync('server/testingCert/cert.pem')
const server = https.createServer({ key: SSLKey, cert: SSLCert }, app)

// Set logging callbacks
server.on('listening', onListening)

// Listen on provided port, on all network interfaces.
server.listen(LISTEN_PORT)

// Event listener for HTTP server "listening" event.
function onListening () {
  const addr = server.address()
  debug('Listening on https://localhost:' + addr.port)
}
janhalama commented 2 months ago

@Olliebrown I have not found any problems with the code you provided.

I can simulate the same error response by configuring the X application as confidential, and then setting the public client type option in my test client code. This is expected, because in the case of the public client type, the `Authorization' header is not sent.

It is the response on the token endpoint that returns the error. The initial code exchange at the authorization endpoint succeeds.

RFC 6749 defines two different methods of client authentication, the first is the authorization header using the basic auth scheme and the second using body params. The node-oauth lib (used under the hood) implements just body params auth and Twitter accepts just auth header. Google and Facebook OAuth 2.0 implementations accept the body params auth, so you have no problems with them.

Our sample app works fine for me, have you tried running it on your dev box?

Olliebrown commented 2 months ago

@janhalama Thank you for taking the time to look through the code! I appreciate it.

I have not tried the simple app so I will give that a go. It is perhaps an issue with my specific box or network environment. I appreciate the lead to follow. I'll report back if I find anything to explain what is happening for me.