mirror of
https://github.com/wassname/talk.git
synced 2026-07-28 11:27:05 +08:00
feat: support facebook auth
This commit is contained in:
Generated
+18
@@ -2080,6 +2080,16 @@
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/passport-facebook": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "http://registry.npmjs.org/@types/passport-facebook/-/passport-facebook-2.1.8.tgz",
|
||||
"integrity": "sha512-5FGF6zNN0ZELetEdIDjVjfHSJfXSehNWeRLv9/8JD6Des4Z9A7sthhyXVRQUXeUxv0SmQ/i+IHZjR8R/G61wIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*",
|
||||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"@types/passport-local": {
|
||||
"version": "1.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz",
|
||||
@@ -18709,6 +18719,14 @@
|
||||
"pause": "0.0.1"
|
||||
}
|
||||
},
|
||||
"passport-facebook": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz",
|
||||
"integrity": "sha1-w50LUq5NWRYyRaTiGnubYyEwMxE=",
|
||||
"requires": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-local": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"nodemailer": "^4.6.7",
|
||||
"nunjucks": "^3.1.3",
|
||||
"passport": "^0.4.0",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2": "^1.4.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
@@ -149,6 +150,7 @@
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"@types/nunjucks": "^3.0.0",
|
||||
"@types/passport": "^0.4.6",
|
||||
"@types/passport-facebook": "^2.1.8",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
"@types/passport-strategy": "^0.2.33",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Db } from "mongodb";
|
||||
import passport, { Authenticator } from "passport";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import FacebookStrategy from "talk-server/app/middleware/passport/strategies/facebook";
|
||||
import { JWTStrategy } from "talk-server/app/middleware/passport/strategies/jwt";
|
||||
import { createLocalStrategy } from "talk-server/app/middleware/passport/strategies/local";
|
||||
import OIDCStrategy from "talk-server/app/middleware/passport/strategies/oidc";
|
||||
@@ -50,6 +51,9 @@ export function createPassport(
|
||||
// Use the SSOStrategy.
|
||||
auth.use(new JWTStrategy(options));
|
||||
|
||||
// Use the FacebookStrategy.
|
||||
auth.use(new FacebookStrategy(options));
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Db } from "mongodb";
|
||||
import {
|
||||
Profile,
|
||||
Strategy as FacebookPassportStrategy,
|
||||
} from "passport-facebook";
|
||||
import { VerifyCallback } from "passport-oauth2";
|
||||
import { Strategy } from "passport-strategy";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { reconstructTenantURL } from "talk-server/app/url";
|
||||
import {
|
||||
GQLFacebookAuthIntegration,
|
||||
GQLUSER_ROLE,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import {
|
||||
FacebookProfile,
|
||||
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;
|
||||
mongo: Db;
|
||||
tenantCache: TenantCache;
|
||||
}
|
||||
|
||||
export default class FacebookStrategy extends Strategy {
|
||||
public name = "facebook";
|
||||
|
||||
private config: Config;
|
||||
private mongo: Db;
|
||||
private cache: TenantCacheAdapter<FacebookPassportStrategy>;
|
||||
|
||||
constructor({ config, mongo, tenantCache }: FacebookStrategyOptions) {
|
||||
super();
|
||||
|
||||
this.config = config;
|
||||
this.mongo = mongo;
|
||||
this.cache = new TenantCacheAdapter(tenantCache);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,16 @@ export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
|
||||
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/facebook",
|
||||
wrapAuthn(options.passport, app.signingConfig, "facebook")
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/facebook/callback",
|
||||
wrapAuthn(options.passport, app.signingConfig, "facebook")
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/oidc/:oidcID",
|
||||
wrapAuthn(options.passport, app.signingConfig, "oidc")
|
||||
|
||||
@@ -32,7 +32,12 @@ export interface SSOProfile {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type Profile = LocalProfile | OIDCProfile | SSOProfile;
|
||||
export interface FacebookProfile {
|
||||
type: "facebook";
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type Profile = LocalProfile | OIDCProfile | SSOProfile | FacebookProfile;
|
||||
|
||||
export interface Token {
|
||||
readonly id: string;
|
||||
|
||||
Reference in New Issue
Block a user