feat: initial passport user auth impl

This commit is contained in:
Wyatt Johnson
2018-07-20 14:30:07 -06:00
parent 2e4d23e900
commit bdd4bfc272
8 changed files with 116 additions and 18 deletions
+11 -2
View File
@@ -5,6 +5,7 @@ import { Db } from "mongodb";
import { notFoundMiddleware } from "talk-server/app/middleware/notFound";
import { createPassport } from "talk-server/app/middleware/passport";
import { JWTSigningConfig } from "talk-server/app/middleware/passport/jwt";
import { Config } from "talk-server/config";
import { handleSubscriptions } from "talk-server/graph/common/subscriptions/middleware";
import { Schemas } from "talk-server/graph/schemas";
@@ -19,6 +20,7 @@ export interface AppOptions {
mongo: Db;
redis: Redis;
schemas: Schemas;
signingConfig: JWTSigningConfig;
}
/**
@@ -32,10 +34,17 @@ export async function createApp(options: AppOptions): Promise<Express> {
parent.use(accessLogger);
// Create some services for the router.
const passport = createPassport({ db: options.mongo });
const passport = createPassport({
db: options.mongo,
signingConfig: options.signingConfig,
});
// Mount the router.
parent.use(await createRouter(options, { passport }));
parent.use(
await createRouter(options, {
passport,
})
);
// Static Files
parent.use(serveStatic);
@@ -3,6 +3,7 @@ import { Db } from "mongodb";
import passport, { Authenticator } from "passport";
import {
createJWTStrategy,
JWTSigningConfig,
SigningTokenOptions,
signTokenString,
@@ -20,10 +21,12 @@ export type VerifyCallback = (
export interface PassportOptions {
db: Db;
signingConfig: JWTSigningConfig;
}
export function createPassport({
db,
signingConfig,
}: PassportOptions): passport.Authenticator {
// Create the authenticator.
const auth = new Authenticator();
@@ -34,6 +37,9 @@ export function createPassport({
// Use the LocalStrategy.
auth.use(createLocalStrategy({ db }));
// Use the JWTStrategy.
auth.use(createJWTStrategy({ db, signingConfig }));
return auth;
}
@@ -74,7 +80,7 @@ export async function handleSuccessfulLogin(
}
/**
* authenticate will wrap a authenticators authenticate method with one that
* wrapAuthz will wrap a authenticators authenticate method with one that
* will return a valid login token for a valid login by a compatible strategy.
*
* @param authenticator the base authenticator instance
@@ -82,7 +88,7 @@ export async function handleSuccessfulLogin(
* @param name the name of the authenticator to use
* @param options any options to be passed to the authenticate call
*/
export const authenticate = (
export const wrapAuthz = (
authenticator: passport.Authenticator,
signingConfig: JWTSigningConfig,
name: string,
+76 -1
View File
@@ -1,8 +1,10 @@
import jwt, { SignOptions } from "jsonwebtoken";
import uuid from "uuid";
import { Db } from "mongodb";
import { Strategy } from "passport-strategy";
import { Config } from "talk-server/config";
import { User } from "talk-server/models/user";
import { retrieveUser, User } from "talk-server/models/user";
import { Request } from "talk-server/types/express";
const authHeaderRegex = /(\S+)\s+(\S+)/;
@@ -126,3 +128,76 @@ export async function signTokenString(
subject: user.id,
});
}
export interface JWTToken {
jti: string;
sub: string;
exp: number;
iss?: string;
}
export interface JWTStrategyOptions {
signingConfig: JWTSigningConfig;
db: Db;
}
export class JWTStrategy extends Strategy {
private signingConfig: JWTSigningConfig;
private db: Db;
public name: string;
constructor({ signingConfig, db }: JWTStrategyOptions) {
super();
this.name = "jwt";
this.signingConfig = signingConfig;
this.db = db;
}
public authenticate(req: Request) {
const { tenant } = req;
if (!tenant) {
// TODO: (wyattjoh) return a better error.
return this.error(new Error("tenant not found"));
}
// Lookup the token.
const token = extractJWTFromRequest(req);
if (!token) {
// TODO: (wyattjoh) return a better error.
return this.fail(new Error("no token on request"), 401);
}
jwt.verify(
token,
// Use the secret specified in the configuration.
this.signingConfig.secret,
{
// We need to verify that the token is for the specified tenant.
issuer: tenant.id,
// Use the algorithm specified in the configuration.
algorithms: [this.signingConfig.algorithm],
},
async (err: Error | undefined, { sub }: JWTToken) => {
if (err) {
return this.fail(err, 401);
}
try {
// Find the user.
const user = await retrieveUser(this.db, tenant.id, sub);
// Return them! The user may be null, but that's ok here.
this.success(user, null);
} catch (err) {
return this.error(err);
}
}
);
}
}
export function createJWTStrategy(options: JWTStrategyOptions) {
return new JWTStrategy(options);
}
@@ -182,7 +182,7 @@ export default class SSOStrategy extends Strategy {
*/
private wrapNewTokenHandler = (tenant: Tenant) => async (
err: Error | undefined,
decoded: OIDCIDToken | SSOToken
token: OIDCIDToken | SSOToken
) => {
if (err) {
return this.fail(err, 401);
@@ -190,7 +190,7 @@ export default class SSOStrategy extends Strategy {
try {
// Find or create the user based on the decoded token.
const user = await this.findOrCreateUser(tenant, decoded);
const user = await this.findOrCreateUser(tenant, token);
// The user was found or created!
return this.success(user, null);
+9 -10
View File
@@ -4,12 +4,11 @@ import passport from "passport";
import { signupHandler } from "talk-server/app/handlers/auth/local";
import { apiErrorHandler } from "talk-server/app/middleware/error";
import { errorLogger } from "talk-server/app/middleware/logging";
import { authenticate } from "talk-server/app/middleware/passport";
import { wrapAuthz } from "talk-server/app/middleware/passport";
import tenantMiddleware from "talk-server/app/middleware/tenant";
import managementGraphMiddleware from "talk-server/graph/management/middleware";
import tenantGraphMiddleware from "talk-server/graph/tenant/middleware";
import { createJWTSigningConfig } from "talk-server/app/middleware/passport/jwt";
import { AppOptions } from "./index";
import playground from "./middleware/playground";
@@ -46,6 +45,9 @@ async function createTenantRouter(app: AppOptions, options: RouterOptions) {
router.use(
"/graphql",
express.json(),
// Any users may submit their GraphQL requests with authentication, this
// middleware will unpack their user into the request.
options.passport.authenticate("jwt", { session: false }),
await tenantGraphMiddleware(app.schemas.tenant, app.config, app.mongo)
);
@@ -55,25 +57,22 @@ async function createTenantRouter(app: AppOptions, options: RouterOptions) {
function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
const router = express.Router();
// Create the signing config.
const signingConfig = createJWTSigningConfig(app.config);
// Mount the passport routes.
router.post(
"/local",
express.json(),
authenticate(options.passport, signingConfig, "local")
wrapAuthz(options.passport, app.signingConfig, "local")
);
router.post(
"/local/signup",
express.json(),
signupHandler({ db: app.mongo, signingConfig })
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
);
router.post("/sso", authenticate(options.passport, signingConfig, "sso"));
router.get("/oidc", authenticate(options.passport, signingConfig, "oidc"));
router.post("/sso", wrapAuthz(options.passport, app.signingConfig, "sso"));
router.get("/oidc", wrapAuthz(options.passport, app.signingConfig, "oidc"));
router.get(
"/oidc/callback",
authenticate(options.passport, signingConfig, "oidc")
wrapAuthz(options.passport, app.signingConfig, "oidc")
);
return router;
+2
View File
@@ -15,12 +15,14 @@ export default class TenantContext extends CommonContext {
public loaders: ReturnType<typeof loaders>;
public mutators: ReturnType<typeof mutators>;
public db: Db;
public user?: User;
public tenant: Tenant;
constructor({ user, tenant, db }: TenantContextOptions) {
super({ user });
this.tenant = tenant;
this.user = user;
this.loaders = loaders(this);
this.mutators = mutators(this);
this.db = db;
@@ -2,7 +2,8 @@ import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generate
const Query: GQLQueryTypeResolver<void> = {
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
settings: (parent, args, ctx) => ctx.tenant,
settings: (source, args, ctx) => ctx.tenant,
me: (source, args, ctx) => ctx.user,
};
export default Query;
+6
View File
@@ -1,9 +1,11 @@
import express, { Express } from "express";
import http from "http";
import { createJWTSigningConfig } from "talk-server/app/middleware/passport/jwt";
import getManagementSchema from "talk-server/graph/management/schema";
import { Schemas } from "talk-server/graph/schemas";
import getTenantSchema from "talk-server/graph/tenant/schema";
import { attachSubscriptionHandlers, createApp, listenAndServe } from "./app";
import config, { Config } from "./config";
import logger from "./logger";
@@ -63,6 +65,9 @@ class Server {
// Setup Redis.
const redis = await createRedisClient(config);
// Create the signing config.
const signingConfig = createJWTSigningConfig(this.config);
// Create the Talk App, branching off from the parent app.
const app: Express = await createApp({
parent,
@@ -70,6 +75,7 @@ class Server {
redis,
config: this.config,
schemas: this.schemas,
signingConfig,
});
// Start the application and store the resulting http.Server.