const bodyParser = require('body-parser') const cookieParser = require('cookie-parser') const session = require('cookie-session') const { OAuthClient, OAuthToken, OAuthCode, User } = require('../models/models') const helpers = require('../../helpers.js') const passport = require('passport') const get = require('lodash/get') const BasicStrategy = require('passport-http').BasicStrategy const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy const ClientPublicStrategy = require('passport-oauth2-client-public').Strategy const BearerStrategy = require('passport-http-bearer').Strategy const AnonymousStrategy = require('passport-anonymous').Strategy const oauth2orize = require('oauth2orize') const log = require('../../log') passport.serializeUser((user, done) => done(null, user.id)) passport.deserializeUser(async (id, done) => { const user = await User.findByPk(id) done(null, user) }) /** * BasicStrategy & ClientPasswordStrategy * * These strategies are used to authenticate registered OAuth clients. They are * employed to protect the `token` endpoint, which consumers use to obtain * access tokens. The OAuth 2.0 specification suggests that clients use the * HTTP Basic scheme to authenticate. Use of the client password strategy * allows clients to send the same credentials in the request body (as opposed * to the `Authorization` header). While this approach is not recommended by * the specification, in practice it is quite common. */ async function verifyClient(client_id, client_secret, done) { const client = await OAuthClient.findByPk(client_id, { raw: true }) if (!client) { return done(null, false) } if (client.client_secret && client_secret !== client.client_secret) { return done(null, false) } if (client) { client.grants = ['authorization_code', 'password'] } //sure ? return done(null, client) } async function verifyPublicClient (client_id, done) { if (client_id !== 'self') { return done(null, false) } try { const client = await OAuthClient.findByPk(client_id, { raw: true }) done(null, client) } catch (e) { done(null, { message: e.message }) } } passport.use(new AnonymousStrategy()) passport.use(new BasicStrategy(verifyClient)) passport.use(new ClientPasswordStrategy(verifyClient)) passport.use(new ClientPublicStrategy(verifyPublicClient)) /** * BearerStrategy * * This strategy is used to authenticate either users or clients based on an access token * (aka a bearer token). If a user, they must have previously authorized a client * application, which is issued an access token to make requests on behalf of * the authorizing user. */ passport.use(new BearerStrategy({ passReqToCallback: true }, verifyToken)) async function verifyToken (req, accessToken, done) { const token = await OAuthToken.findByPk(accessToken, { include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] }) if (!token) return done(null, false) if (token.userId) { if (!token.user) { return done(null, false) } // To keep this example simple, restricted scopes are not implemented, // and this is just for illustrative purposes. done(null, token.user, { scope: '*' }) } else { // The request came from a client only since userId is null, // therefore the client is passed back instead of a user. if (!token.client) { return done(null, false) } // To keep this example simple, restricted scopes are not implemented, // and this is just for illustrative purposes. done(null, client, { scope: '*' }) } } const oauthServer = oauth2orize.createServer() // Register serialization and deserialization functions. // // When a client redirects a user to user authorization endpoint, an // authorization transaction is initiated. To complete the transaction, the // user must authenticate and approve the authorization request. Because this // may involve multiple HTTP request/response exchanges, the transaction is // stored in the session. // // An application must supply serialization functions, which determine how the // client object is serialized into the session. Typically this will be a // simple matter of serializing the client's ID, and deserializing by finding // the client by ID from the database. oauthServer.serializeClient((client, done) => { done(null, client.id) }) oauthServer.deserializeClient(async (id, done) => { const client = await OAuthClient.findByPk(id) done(null, client) }) // Register supported grant types. // // OAuth 2.0 specifies a framework that allows users to grant client // applications limited access to their protected resources. It does this // through a process of the user granting access, and the client exchanging // the grant for an access token. // Grant authorization codes. The callback takes the `client` requesting // authorization, the `redirectUri` (which is used as a verifier in the // subsequent exchange), the authenticated `user` granting access, and // their response, which contains approved scope, duration, etc. as parsed by // the application. The application issues a code, which is bound to these // values, and will be exchanged for an access token. oauthServer.grant(oauth2orize.grant.code(async (client, redirect_uri, user, ares, done) => { const authorizationCode = helpers.randomString(16); await OAuthCode.create({ redirect_uri, authorizationCode, clientId: client.id, userId: user.id, }) return done(null, authorizationCode) })) // Grant implicit authorization. The callback takes the `client` requesting // authorization, the authenticated `user` granting access, and // their response, which contains approved scope, duration, etc. as parsed by // the application. The application issues a token, which is bound to these // values. oauthServer.grant(oauth2orize.grant.token((client, user, ares, done) => { return oauthController.issueTokens(user.id, client.clientId, done) })) // Exchange authorization codes for access tokens. The callback accepts the // `client`, which is exchanging `code` and any `redirectUri` from the // authorization request for verification. If these values are validated, the // application issues an access token on behalf of the user who authorized the // code. The issued access token response can include a refresh token and // custom parameters by adding these to the `done()` call oauthServer.exchange(oauth2orize.exchange.code(async (client, code, redirect_uri, done) => { const oauthCode = await OAuthCode.findByPk(code) if (!oauthCode || client.id !== oauthCode.clientId || client.redirectUris !== oauthCode.redirect_uri) { return done(null, false) } return oauthController.issueTokens(oauthCode.userId, oauthCode.clientId, done) })) // Exchange user id and password for access tokens. The callback accepts the // `client`, which is exchanging the user's name and password from the // authorization request for verification. If these values are validated, the // application issues an access token on behalf of the user who authorized the code. oauthServer.exchange(oauth2orize.exchange.password(async (client, username, password, scope, done) => { // Validate the client const oauthClient = await OAuthClient.findByPk(client.id) if (!oauthClient) { // || oauthClient.client_secret !== client.clientSecret) { return done(null, false) } const user = await User.findOne({ where: { email: username, is_active: true } }) if (!user) { return done(null, false) } // check if password matches if (await user.comparePassword(password)) { return oauthController.issueTokens(user.id, oauthClient.id, done) } return done(null, false) })) // Exchange the client id and password/secret for an access token. The callback accepts the // `client`, which is exchanging the client's id and password/secret from the // authorization request for verification. If these values are validated, the // application issues an access token on behalf of the client who authorized the code. oauthServer.exchange(oauth2orize.exchange.clientCredentials(async (client, scope, done) => { // Validate the client const oauthClient = await OAuthClient.findByPk(client.clientId) if (!oauthClient || oauthClient.client_secret !== client.clientSecret) { return done(null, false) } return oauthController.issueTokens(null, oauthClient.id, done) })) // issue new tokens and remove the old ones oauthServer.exchange(oauth2orize.exchange.refreshToken(async (client, refreshToken, scope, done) => { // db.refreshTokens.find(refreshToken, (error, token) => { // if (error) return done(error) // issueTokens(token.id, client.id, (err, accessToken, refreshToken) => { // if (err) { // done(err, null, null) // } // db.accessTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => { // if (err) { // done(err, null, null) // } // db.refreshTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => { // if (err) { // done(err, null, null) // } // done(null, accessToken, refreshToken) // }) // }) // }) // }) })) const oauthController = { // this is a middleware to authenticate a request authenticate: [ passport.initialize(), // initialize passport cookieParser(), // parse cookies session({ secret: 'secret', resave: true, saveUninitialized: true }), passport.session(), (req, res, next) => { // retrocompatibility const token = get(req.cookies, 'auth._token.local', null) const authorization = get(req.headers, 'authorization', null) if (!authorization && token) { req.headers.authorization = token } next() }, passport.authenticate(['bearer', 'oauth2-client-password', 'anonymous'], { session: false }) ], login: [ bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form passport.authenticate(['oauth2-client-public'], { session: false }), oauthServer.token(), oauthServer.errorHandler() ], token: [ bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form passport.authenticate(['bearer', 'oauth2-client-password'], { session: false }), oauthServer.token(), oauthServer.errorHandler() ], authorization: [ oauthServer.authorization(async (clientId, redirectUri, done) => { const oauthClient = await OAuthClient.findByPk(clientId) if (!oauthClient) { return done(null, false) } // WARNING: For security purposes, it is highly advisable to check that // redirectUri provided by the client matches one registered with // the server. For simplicity, this example does not. You have // been warned. return done(null, oauthClient, redirectUri); }, async (client, user, done) => { // Check if grant request qualifies for immediate approval // Auto-approve if (client.isTrusted) return done(null, true); if (!user) { return done(null, false) } const token = await OAuthToken.findOne({ where: { clientId: client.id, userId: user.id }}) // Auto-approve if (token) { return done(null, true) } // Otherwise ask user return done(null, false) }), (req, res, next) => { //clean old transactionID if(req.session.authorize){ for(const key in req.session.authorize){ if(key !== req.oauth2.transactionID){ delete req.session.authorize[key]; } } } const query = new URLSearchParams({ transactionID: req.oauth2.transactionID, client: req.oauth2.client.name, scope: req.oauth2.client.scopes, redirect_uri: req.oauth2.client.redirectUris }) return res.redirect(`/authorize?${query.toString()}`) } ], decision: [ bodyParser.urlencoded({ extended: true }), oauthServer.decision() ], async issueTokens(userId, clientId, done) { const user = await User.findByPk(userId) if (!user) { return done(null, false) } const refreshToken = helpers.randomString(32) const accessToken = helpers.randomString(32) const token = { refreshToken, accessToken, userId, clientId } await OAuthToken.create(token) return done(null, accessToken, refreshToken, { username: user.email }) }, // create client => http:///gancio.org/dev/oauth#create-client async createClient (req, res) { // only write scope is supported if (req.body.scopes && req.body.scopes !== 'event:write') { return res.status(422).json({ error: 'Invalid scopes' }) } const client = { id: helpers.randomString(32), name: req.body.client_name, redirectUris: req.body.redirect_uris, scopes: req.body.scopes || 'event:write', website: req.body.website, client_secret: helpers.randomString(32) } try { await OAuthClient.create(client) client.client_id = client.id delete client.id res.json(client) } catch (e) { log.error('[OAUTH CLIENT]', e) res.status(400).json(e) } }, async getClient (req, res) { const client = await OAuthClient.findByPk(req.params.client_id, { raw: true }) if (!client) { return res.status(404).send('Not found!') } res.json({ client_id: client.id, redirect_uris: client.redirectUris, name: client.name, website: client.website, scopes: client.scopes }) }, async getClients (req, res) { const tokens = await OAuthToken.findAll({ include: [{ model: User, where: { id: req.user.id } }, { model: OAuthClient, as: 'client' }], raw: true, nest: true }) res.json(tokens) } } module.exports = oauthController