Last active
October 6, 2022 20:50
-
-
Save jaredpalmer/eded6f13d8c106a916212ffcce4625d1 to your computer and use it in GitHub Desktop.
Express OAuth2 Provider example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import * as uuid from 'uuid'; | |
import { Client, ClientDao } from '../dao/ClientDao'; | |
import { Token, TokenDao } from '../dao/TokenDao'; | |
import { User, UserDao } from '../dao/UserDao'; | |
export const getAccessToken = async (bearerToken: string) => { | |
const token = await TokenDao.loadByToken(bearerToken); | |
const user = await UserDao.load(token.userId); | |
delete user.password; | |
return { | |
accessTokenExpiresAt: token.accessTokenExpiresAt, | |
accessToken: token.accessToken, | |
user, | |
scope: 'basic', | |
}; | |
}; | |
export const getClient = (clientId: string, clientSecret: string) => { | |
return ClientDao.loadByClientIdAndSecret(clientId, clientSecret).then( | |
(client: any) => { | |
if (!client) { | |
return false; | |
} | |
return { | |
clientId: client.clientId, | |
clientSecret: client.clientSecret, | |
redirectUris: client.redirectUris, | |
grants: ['password'], | |
}; | |
} | |
); | |
}; | |
export const getRefreshToken = (refreshToken: string) => { | |
return TokenDao.loadByRefreshToken(refreshToken); | |
}; | |
export const getUser = async (username: string, password: string) => { | |
const user = await UserDao.loadByEmail(username); | |
if (!user) { | |
return false; | |
} | |
const isMatch = await UserDao.verifyPassword(user.password, password); | |
if (!isMatch) { | |
return false; | |
} | |
delete user.password; | |
return user; | |
}; | |
export const saveToken = async (token: Token, client: Client, user: User) => { | |
const newToken = await TokenDao.create({ | |
accessToken: token.accessToken, | |
accessTokenExpiresAt: token.accessTokenExpiresAt, | |
clientId: client.clientId, | |
refreshToken: token.refreshToken, | |
refreshTokenExpiresAt: token.refreshTokenExpiresAt, | |
scope: token.scope, | |
userId: user.id, | |
}); | |
delete user.password; | |
return { | |
accessToken: newToken.accessToken, | |
accessTokenExpiresAt: newToken.accessTokenExpiresAt, | |
refreshToken: newToken.refreshToken, | |
refreshTokenExpiresAt: newToken.refreshTokenExpiresAt, | |
client, | |
scope: newToken.scope, | |
user, | |
}; | |
}; | |
export const validateScope = (_user: User, _client: Client, scope: string) => { | |
// we don't have any scopes that we | |
// enforce yet. We will probably want | |
// to eventually store this as a field on | |
// tokens. | |
return scope || 'basic'; | |
}; | |
export const generateAccessToken = () => Promise.resolve(uuid.v4()); | |
export const generateRefreshToken = () => Promise.resolve(uuid.v4()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// If you are using apollo-server, DO NOT use the ensureAuth middleware on your | |
// Graphql endpoint. Instead, use the function to grab the token out of the request | |
// header and load the user from it by hand. | |
import { Request } from 'express'; | |
import { TokenDao } from './dao/TokenDao'; | |
import { UserDao } from './dao/UserDao'; | |
import { isBefore, parse } from 'date-fns'; | |
export async function loadContextFromRequest(req: Request) { | |
try { | |
let viewer; | |
let token; | |
const reqToken = | |
req.headers && | |
req.headers.authorization && | |
(req.headers.authorization as string).replace(/^\s*Bearer\s*/, ''); | |
if (reqToken) { | |
token = await TokenDao.loadByToken(reqToken); | |
if (isBefore(parse(Date.now()), parse(token.accessTokenExpiresAt))) { | |
viewer = await UserDao.load(token.userId); | |
} | |
} | |
return { viewer, token }; | |
} catch (error) { | |
return undefined; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require('dotenv').config({ silent: process.env.NODE_ENV !== 'production' }); | |
const debug = require('debug')('api'); | |
debug('logging with debug enabled!'); | |
import bodyParser from 'body-parser'; | |
import compression from 'compression'; | |
import cors from 'cors'; | |
import express from 'express'; | |
import morgan from 'morgan'; | |
import throng from 'throng'; | |
import { Request, Response, NextFunction } from './types'; | |
import * as AuthModel from './models/AuthModel'; | |
const OAuthServer = require('express-oauth-server'); | |
export const createServer = (_instance: number) => { | |
const app = express(); | |
const oauth = new OAuthServer({ | |
model: AuthModel, | |
}); | |
app | |
.disable('x-powered-by') | |
.use(compression()) | |
.use(cors()) | |
.use(bodyParser.json()) | |
.use(bodyParser.urlencoded({ extended: true })) | |
.use(morgan(process.env.NODE_ENV !== 'production' ? 'dev' : 'combined')); | |
const ensureAuth = () => [ | |
oauth.authenticate(), | |
(req: Request<any>, res: Response, next: NextFunction) => { | |
// simplify access to authenticated user | |
req.user = (res as any).locals.oauth.token.user || undefined; | |
req.scope = (res as any).locals.oauth.token.scope || undefined; | |
req.token = (res as any).locals.oauth.token.accessToken || undefined; | |
next(); | |
}, | |
]; | |
// public | |
app.get('/', (_req, res) => res.json({ hello: 'world' })); | |
app.get('/__health', (_req, res) => res.json({ status: 'alive' })); | |
app.post('/oauth/token', oauth.token()); | |
// private | |
app.get('/v1/user/me', ensureAuth(), UserController.me); | |
app.get('/v1/client/:id', ensureAuth(), ClientController.load); | |
app.post('/v1/client', ensureAuth(), ClientController.create); | |
return app; | |
}; | |
function startServer(id: number): void { | |
const server = createServer(id); | |
server.listen(process.env.PORT || 5000, () => { | |
console.log('> starting server', id, process.env.PORT || 5000); | |
}); | |
} | |
if (require.main === module) { | |
throng(2, startServer); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// I can't share the DAO's from above but here are the basic interfaces. | |
export interface Token { | |
/** Unique client API key */ | |
clientId: string; | |
/** Actual OAuth token */ | |
accessToken: string; | |
/** Token Expire At */ | |
accessTokenExpiresAt: Date; | |
/** Refresh OAuth token */ | |
refreshToken: string; | |
/** Refresh Token Expire At */ | |
refreshTokenExpiresAt: Date; | |
/** Token scope */ | |
scope: 'basic'; | |
/** User identifier */ | |
userId: IObjectID; | |
} | |
export interface Client { | |
/** Unique Client API Key*/ | |
clientId: string; | |
/** Client secret token */ | |
clientSecret: string; | |
/** Acceptable redirect URLs */ | |
redirectUris: string[]; | |
} | |
export interface User { | |
/** User email */ | |
email: string; | |
/** User password hash */ | |
password: string; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment