Skip to content

Instantly share code, notes, and snippets.

@jaredpalmer
Last active October 6, 2022 20:50
Show Gist options
  • Save jaredpalmer/eded6f13d8c106a916212ffcce4625d1 to your computer and use it in GitHub Desktop.
Save jaredpalmer/eded6f13d8c106a916212ffcce4625d1 to your computer and use it in GitHub Desktop.
Express OAuth2 Provider example
'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());
// 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;
}
}
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);
}
// 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