mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 15:56:01 +08:00
feat: created oauth2 abstract class
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import { Db } from "mongodb";
|
||||
import {
|
||||
Profile,
|
||||
Strategy as FacebookPassportStrategy,
|
||||
} from "passport-facebook";
|
||||
import { VerifyCallback } from "passport-oauth2";
|
||||
import { Strategy } from "passport-strategy";
|
||||
import { Profile, Strategy } from "passport-facebook";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import OAuth2Strategy from "talk-server/app/middleware/passport/strategies/oauth2";
|
||||
import { reconstructTenantURL } from "talk-server/app/url";
|
||||
import {
|
||||
GQLAuthIntegrations,
|
||||
GQLFacebookAuthIntegration,
|
||||
GQLUSER_ROLE,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
@@ -18,85 +15,7 @@ import {
|
||||
retrieveUserWithProfile,
|
||||
} from "talk-server/models/user";
|
||||
import TenantCache from "talk-server/services/tenant/cache";
|
||||
import { TenantCacheAdapter } from "talk-server/services/tenant/cache/adapter";
|
||||
import { upsert } from "talk-server/services/users";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
async function findOrCreateFacebookUser(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
integration: GQLFacebookAuthIntegration,
|
||||
{ id, photos, emails, displayName }: Profile
|
||||
) {
|
||||
// Create the user profile that will be used to lookup the User.
|
||||
const profile: FacebookProfile = {
|
||||
type: "facebook",
|
||||
id,
|
||||
};
|
||||
|
||||
let user = await retrieveUserWithProfile(mongo, tenant.id, profile);
|
||||
if (!user) {
|
||||
if (!integration.allowRegistration) {
|
||||
// Registration is disabled, so we can't create the user user here.
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: implement rules.
|
||||
|
||||
// Try to get the avatar.
|
||||
let avatar: string | undefined;
|
||||
if (photos && photos.length > 0) {
|
||||
avatar = photos[0].value;
|
||||
}
|
||||
|
||||
// Try to get the email address.
|
||||
let email: string | undefined;
|
||||
let emailVerified: boolean | undefined;
|
||||
if (emails && emails.length > 0) {
|
||||
email = emails[0].value;
|
||||
emailVerified = false;
|
||||
}
|
||||
|
||||
user = await upsert(mongo, tenant, {
|
||||
username: null,
|
||||
displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
email_verified: emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function getEnabledIntegration(
|
||||
tenant: Tenant
|
||||
): Required<GQLFacebookAuthIntegration> {
|
||||
const integration = tenant.auth.integrations.facebook;
|
||||
|
||||
// Handle when the integration is enabled/disabled.
|
||||
if (!integration.enabled) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("integration not enabled");
|
||||
}
|
||||
if (!integration.enabled) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("integration not enabled");
|
||||
}
|
||||
|
||||
if (!integration.clientID) {
|
||||
throw new Error("clientID is missing in configuration");
|
||||
}
|
||||
|
||||
if (!integration.clientSecret) {
|
||||
throw new Error("clientSecret is missing in configuration");
|
||||
}
|
||||
|
||||
// TODO: (wyattjoh) for some reason, type guards above to not allow coercion to this required type.
|
||||
return integration as Required<GQLFacebookAuthIntegration>;
|
||||
}
|
||||
|
||||
export interface FacebookStrategyOptions {
|
||||
config: Config;
|
||||
@@ -104,100 +23,81 @@ export interface FacebookStrategyOptions {
|
||||
tenantCache: TenantCache;
|
||||
}
|
||||
|
||||
export default class FacebookStrategy extends Strategy {
|
||||
export default class FacebookStrategy extends OAuth2Strategy<
|
||||
GQLFacebookAuthIntegration,
|
||||
Strategy
|
||||
> {
|
||||
public name = "facebook";
|
||||
|
||||
private config: Config;
|
||||
private mongo: Db;
|
||||
private cache: TenantCacheAdapter<FacebookPassportStrategy>;
|
||||
protected getIntegration = (integrations: GQLAuthIntegrations) =>
|
||||
integrations.facebook;
|
||||
|
||||
constructor({ config, mongo, tenantCache }: FacebookStrategyOptions) {
|
||||
super();
|
||||
protected async findOrCreateUser(
|
||||
tenant: Tenant,
|
||||
integration: Required<GQLFacebookAuthIntegration>,
|
||||
{ id, photos, emails, displayName }: Profile
|
||||
) {
|
||||
// Create the user profile that will be used to lookup the User.
|
||||
const profile: FacebookProfile = {
|
||||
type: "facebook",
|
||||
id,
|
||||
};
|
||||
|
||||
this.config = config;
|
||||
this.mongo = mongo;
|
||||
this.cache = new TenantCacheAdapter(tenantCache);
|
||||
let user = await retrieveUserWithProfile(this.mongo, tenant.id, profile);
|
||||
if (!user) {
|
||||
if (!integration.allowRegistration) {
|
||||
// Registration is disabled, so we can't create the user user here.
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: implement rules.
|
||||
|
||||
// Try to get the avatar.
|
||||
let avatar: string | undefined;
|
||||
if (photos && photos.length > 0) {
|
||||
avatar = photos[0].value;
|
||||
}
|
||||
|
||||
// Try to get the email address.
|
||||
let email: string | undefined;
|
||||
let emailVerified: boolean | undefined;
|
||||
if (emails && emails.length > 0) {
|
||||
email = emails[0].value;
|
||||
emailVerified = false;
|
||||
}
|
||||
|
||||
user = await upsert(this.mongo, tenant, {
|
||||
username: null,
|
||||
displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
email_verified: emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private userAuthenticatedCallback = async (
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
done: VerifyCallback
|
||||
) => {
|
||||
const { tenant } = req.talk!;
|
||||
if (!tenant) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("tenant not found");
|
||||
}
|
||||
|
||||
// Get the integration from the tenant. If needed, it will be used to create
|
||||
// a new strategy.
|
||||
let integration: Required<GQLFacebookAuthIntegration>;
|
||||
try {
|
||||
integration = getEnabledIntegration(tenant);
|
||||
} catch (err) {
|
||||
// TODO: wrap error?
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await findOrCreateFacebookUser(
|
||||
this.mongo,
|
||||
tenant,
|
||||
integration,
|
||||
profile
|
||||
);
|
||||
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
};
|
||||
|
||||
public authenticate(req: Request) {
|
||||
try {
|
||||
const { tenant } = req.talk!;
|
||||
if (!tenant) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("tenant not found");
|
||||
}
|
||||
|
||||
// Get the integration (it will throw if the integration is not enabled).
|
||||
const integration = getEnabledIntegration(tenant);
|
||||
|
||||
let strategy = this.cache.get(tenant.id);
|
||||
if (!strategy) {
|
||||
strategy = new FacebookPassportStrategy(
|
||||
{
|
||||
clientID: integration.clientID,
|
||||
clientSecret: integration.clientSecret,
|
||||
callbackURL: reconstructTenantURL(
|
||||
this.config,
|
||||
tenant,
|
||||
"/api/tenant/auth/facebook/callback"
|
||||
),
|
||||
profileFields: ["id", "displayName", "photos", "email"],
|
||||
enableProof: true,
|
||||
passReqToCallback: true,
|
||||
},
|
||||
this.userAuthenticatedCallback
|
||||
);
|
||||
|
||||
this.cache.set(tenant.id, strategy);
|
||||
}
|
||||
|
||||
// Augment the strategy with the request method bindings.
|
||||
strategy.error = this.error.bind(this);
|
||||
strategy.fail = this.fail.bind(this);
|
||||
strategy.pass = this.pass.bind(this);
|
||||
strategy.redirect = this.redirect.bind(this);
|
||||
strategy.success = this.success.bind(this);
|
||||
|
||||
strategy.authenticate(req, { session: false });
|
||||
} catch (err) {
|
||||
return this.error(err);
|
||||
}
|
||||
protected createStrategy(
|
||||
tenant: Tenant,
|
||||
integration: Required<GQLFacebookAuthIntegration>
|
||||
) {
|
||||
return new Strategy(
|
||||
{
|
||||
clientID: integration.clientID,
|
||||
clientSecret: integration.clientSecret,
|
||||
callbackURL: reconstructTenantURL(
|
||||
this.config,
|
||||
tenant,
|
||||
"/api/tenant/auth/facebook/callback"
|
||||
),
|
||||
profileFields: ["id", "displayName", "photos", "email"],
|
||||
enableProof: true,
|
||||
passReqToCallback: true,
|
||||
},
|
||||
this.verifyCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Db } from "mongodb";
|
||||
import { Strategy } from "passport-strategy";
|
||||
|
||||
import { Profile } from "passport";
|
||||
import { VerifyCallback } from "passport-oauth2";
|
||||
import { Config } from "talk-common/config";
|
||||
import { GQLAuthIntegrations } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
import TenantCache from "talk-server/services/tenant/cache";
|
||||
import { TenantCacheAdapter } from "talk-server/services/tenant/cache/adapter";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
interface OAuth2Integration {
|
||||
enabled: boolean;
|
||||
clientID?: string;
|
||||
clientSecret?: string;
|
||||
}
|
||||
|
||||
export interface OAuth2StrategyOptions {
|
||||
config: Config;
|
||||
mongo: Db;
|
||||
tenantCache: TenantCache;
|
||||
}
|
||||
|
||||
export default abstract class OAuth2Strategy<
|
||||
T extends OAuth2Integration,
|
||||
U extends Strategy
|
||||
> extends Strategy {
|
||||
protected config: Config;
|
||||
protected mongo: Db;
|
||||
protected cache: TenantCacheAdapter<U>;
|
||||
|
||||
constructor({ config, mongo, tenantCache }: OAuth2StrategyOptions) {
|
||||
super();
|
||||
|
||||
this.config = config;
|
||||
this.mongo = mongo;
|
||||
this.cache = new TenantCacheAdapter(tenantCache);
|
||||
}
|
||||
|
||||
protected abstract getIntegration(integrations: GQLAuthIntegrations): T;
|
||||
|
||||
protected abstract createStrategy(
|
||||
tenant: Tenant,
|
||||
integration: Required<T>
|
||||
): U;
|
||||
|
||||
protected abstract findOrCreateUser(
|
||||
tenant: Tenant,
|
||||
integration: Required<T>,
|
||||
profile: Profile
|
||||
): Promise<User | undefined>;
|
||||
|
||||
protected verifyCallback = async (
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
done: VerifyCallback
|
||||
) => {
|
||||
try {
|
||||
// Talk is defined at this point.
|
||||
const tenant = req.talk!.tenant!;
|
||||
|
||||
// Get the integration.
|
||||
const integration = this.getIntegration(tenant.auth.integrations);
|
||||
|
||||
// Get the user.
|
||||
const user = await this.findOrCreateUser(
|
||||
tenant,
|
||||
integration as Required<T>,
|
||||
profile
|
||||
);
|
||||
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
};
|
||||
|
||||
public authenticate(req: Request) {
|
||||
try {
|
||||
// Talk is defined at this point.
|
||||
const tenant = req.talk!.tenant!;
|
||||
|
||||
// Get the integration.
|
||||
const integration = this.getIntegration(tenant.auth.integrations);
|
||||
|
||||
// Check to see if the integration is enabled.
|
||||
if (!integration.enabled) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("integration not enabled");
|
||||
}
|
||||
|
||||
if (!integration.clientID) {
|
||||
throw new Error("clientID is missing in configuration");
|
||||
}
|
||||
|
||||
if (!integration.clientSecret) {
|
||||
throw new Error("clientSecret is missing in configuration");
|
||||
}
|
||||
|
||||
let strategy = this.cache.get(tenant.id);
|
||||
if (!strategy) {
|
||||
strategy = this.createStrategy(tenant, integration as Required<T>);
|
||||
this.cache.set(tenant.id, strategy);
|
||||
}
|
||||
|
||||
// Augment the strategy with the request method bindings.
|
||||
strategy.error = this.error.bind(this);
|
||||
strategy.fail = this.fail.bind(this);
|
||||
strategy.pass = this.pass.bind(this);
|
||||
strategy.redirect = this.redirect.bind(this);
|
||||
strategy.success = this.success.bind(this);
|
||||
|
||||
strategy.authenticate(req, { session: false });
|
||||
} catch (err) {
|
||||
return this.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user