mirror of
https://github.com/wassname/talk.git
synced 2026-07-21 12:51:03 +08:00
feat: initial passport user auth impl
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user