feat: added google support

This commit is contained in:
Wyatt Johnson
2018-10-29 17:36:15 -06:00
parent 58e51b299d
commit 93b91a0573
9 changed files with 207 additions and 22 deletions
+8
View File
@@ -18727,6 +18727,14 @@
"passport-oauth2": "1.x.x"
}
},
"passport-google-oauth2": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/passport-google-oauth2/-/passport-google-oauth2-0.1.6.tgz",
"integrity": "sha1-39cBasdEn+J8/rJSrpdK/CMleg0=",
"requires": {
"passport-oauth2": "^1.1.2"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+1
View File
@@ -91,6 +91,7 @@
"nunjucks": "^3.1.3",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth2": "^0.1.6",
"passport-local": "^1.0.0",
"passport-oauth2": "^1.4.0",
"passport-strategy": "^1.0.0",
@@ -7,6 +7,7 @@ import passport, { Authenticator } from "passport";
import { Config } from "talk-common/config";
import FacebookStrategy from "talk-server/app/middleware/passport/strategies/facebook";
import GoogleStrategy from "talk-server/app/middleware/passport/strategies/google";
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";
@@ -54,6 +55,9 @@ export function createPassport(
// Use the FacebookStrategy.
auth.use(new FacebookStrategy(options));
// Use the GoogleStrategy.
auth.use(new GoogleStrategy(options));
return auth;
}
@@ -77,6 +77,8 @@ export default class FacebookStrategy extends OAuth2Strategy<
});
}
// TODO: maybe update user details?
return user;
}
@@ -0,0 +1,116 @@
import { Db } from "mongodb";
import { Profile, Strategy } from "passport-google-oauth2";
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,
GQLGoogleAuthIntegration,
GQLUSER_ROLE,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { Tenant } from "talk-server/models/tenant";
import {
GoogleProfile,
retrieveUserWithProfile,
} from "talk-server/models/user";
import TenantCache from "talk-server/services/tenant/cache";
import { upsert } from "talk-server/services/users";
export interface GoogleStrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
}
export interface GoogleStrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
}
export default class GoogleStrategy extends OAuth2Strategy<
GQLGoogleAuthIntegration,
Strategy
> {
public name = "google";
constructor(options: GoogleStrategyOptions) {
super({
...options,
scope: ["profile"],
});
}
protected getIntegration = (integrations: GQLAuthIntegrations) =>
integrations.google;
protected async findOrCreateUser(
tenant: Tenant,
integration: Required<GQLGoogleAuthIntegration>,
{ id, photos, emails, displayName }: Profile
) {
// Create the user profile that will be used to lookup the User.
const profile: GoogleProfile = {
type: "google",
id,
};
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],
});
}
// TODO: maybe update user details?
return user;
}
protected createStrategy(
tenant: Tenant,
integration: Required<GQLGoogleAuthIntegration>
) {
return new Strategy(
{
clientID: integration.clientID,
clientSecret: integration.clientSecret,
callbackURL: reconstructTenantURL(
this.config,
tenant,
"/api/tenant/auth/google/callback"
),
passReqToCallback: true,
},
this.verifyCallback
);
}
}
@@ -21,6 +21,7 @@ export interface OAuth2StrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
scope?: string[];
}
export default abstract class OAuth2Strategy<
@@ -30,13 +31,15 @@ export default abstract class OAuth2Strategy<
protected config: Config;
protected mongo: Db;
protected cache: TenantCacheAdapter<U>;
private scope?: string[];
constructor({ config, mongo, tenantCache }: OAuth2StrategyOptions) {
constructor({ config, mongo, tenantCache, scope }: OAuth2StrategyOptions) {
super();
this.config = config;
this.mongo = mongo;
this.cache = new TenantCacheAdapter(tenantCache);
this.scope = scope;
}
protected abstract getIntegration(integrations: GQLAuthIntegrations): T;
@@ -114,7 +117,10 @@ export default abstract class OAuth2Strategy<
strategy.redirect = this.redirect.bind(this);
strategy.success = this.success.bind(this);
strategy.authenticate(req, { session: false });
strategy.authenticate(req, {
session: false,
scope: this.scope,
});
} catch (err) {
return this.error(err);
}
+19 -19
View File
@@ -8,16 +8,30 @@ import {
import { wrapAuthn } from "talk-server/app/middleware/passport";
import { RouterOptions } from "talk-server/app/router/types";
function wrapPath(
app: AppOptions,
options: RouterOptions,
router: express.Router,
strategy: string,
path: string = `/${strategy}`
) {
const handler = wrapAuthn(options.passport, app.signingConfig, strategy);
router.get(path, handler);
router.get(path + "/callback", handler);
}
export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
const router = express.Router();
// Mount the passport routes.
// Mount the logout handler.
router.delete(
"/",
options.passport.authenticate("jwt", { session: false }),
logoutHandler({ redis: app.redis })
);
// Mount the Local Authentication handlers.
router.post(
"/local",
express.json(),
@@ -29,24 +43,10 @@ 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")
);
router.get(
"/oidc/:oidcID/callback",
wrapAuthn(options.passport, app.signingConfig, "oidc")
);
// Mount the external auth integrations with middleware/handle wrappers.
wrapPath(app, options, router, "facebook");
wrapPath(app, options, router, "google");
wrapPath(app, options, router, "oidc", "/oidc/:oidc");
return router;
}
+11 -1
View File
@@ -37,7 +37,17 @@ export interface FacebookProfile {
id: string;
}
export type Profile = LocalProfile | OIDCProfile | SSOProfile | FacebookProfile;
export interface GoogleProfile {
type: "google";
id: string;
}
export type Profile =
| LocalProfile
| OIDCProfile
| SSOProfile
| FacebookProfile
| GoogleProfile;
export interface Token {
readonly id: string;
+38
View File
@@ -0,0 +1,38 @@
declare module "passport-google-oauth2" {
import express from "express";
import passport from "passport";
export interface Profile extends passport.Profile {
id: string;
displayName: string;
}
export interface AuthenticateOptions extends passport.AuthenticateOptions {
authType?: string;
}
export interface StrategyOptionWithRequest {
clientID: string;
clientSecret: string;
callbackURL: string;
passReqToCallback: true;
}
export type VerifyFunctionWithRequest = (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: Profile,
done: (error: any, user?: any, info?: any) => void
) => void;
export class Strategy extends passport.Strategy {
constructor(
options: StrategyOptionWithRequest,
verify: VerifyFunctionWithRequest
);
public name: string;
public authenticate(req: express.Request, options?: object): void;
}
}