import { Service, Inject } from "@fullstack-one/di"; import { SchemaBuilder } from "@fullstack-one/schema-builder"; import { GraphQl } from "@fullstack-one/graphql"; import { Auth, AuthProvider } from ".."; import { Server } from "@fullstack-one/server"; import { Config } from "@fullstack-one/config"; import { BootLoader } from "@fullstack-one/boot-loader"; import * as KoaRouter from "koa-router"; import * as koaBody from "koa-bodyparser"; import * as koaSession from "koa-session"; import * as passport from "koa-passport"; import { LoggerFactory, Logger } from "@fullstack-one/logger"; import { ORM } from "@fullstack-one/db"; const template = ` Authetication

This Page requires Javascript.

`; const oAuthCallback = (message, origins) => { const data = { message, origins }; return template.replace("<%=data%>", JSON.stringify(data)); }; @Service() export class AuthProviderOAuth { private emailAuthProvider: AuthProvider; private oAuthAuthProviders: { [key: string]: AuthProvider } = {}; private server: Server; private logger: Logger; private orm: ORM; private authConfig; constructor( @Inject((type) => SchemaBuilder) schemaBuilder: SchemaBuilder, @Inject((type) => GraphQl) graphQl: GraphQl, @Inject((type) => Auth) auth: Auth, @Inject((type) => ORM) orm: ORM, @Inject((type) => Server) server: Server, @Inject((type) => BootLoader) bootLoader: BootLoader, @Inject((type) => Config) config: Config, @Inject((type) => LoggerFactory) loggerFactory: LoggerFactory ) { this.server = server; this.orm = orm; this.authConfig = config.getConfig("Auth"); this.logger = loggerFactory.create("OAuthAuthProvider"); this.emailAuthProvider = auth.createAuthProvider("email"); Object.keys(this.authConfig.oAuth.providers).forEach((key) => { this.oAuthAuthProviders[key] = auth.createAuthProvider(key); }); bootLoader.addBootFunction(this.constructor.name, this.boot.bind(this)); } private async boot() { const authRouter = new KoaRouter(); const app = this.server.getApp(); authRouter.use(koaBody()); app.keys = [this.authConfig.secrets.cookie]; authRouter.use(koaSession(this.authConfig.oAuth.cookie, app)); authRouter.use(passport.initialize()); authRouter.get("/auth/oAuthFailure", async (ctx) => { const message = { err: "ERROR_AUTH", data: null }; ctx.body = oAuthCallback(message, this.authConfig.oAuth.frontendOrigins); }); authRouter.get("/auth/oAuthFailure/:err", async (ctx) => { const message = { err: ctx.params.err, data: null }; ctx.body = oAuthCallback(message, this.authConfig.oAuth.frontendOrigins); }); authRouter.get("/auth/oAuthSuccess/:data", async (ctx) => { const message = { err: null, data: JSON.parse(ctx.params.data) }; ctx.body = oAuthCallback(message, this.authConfig.oAuth.frontendOrigins); }); Object.keys(this.authConfig.oAuth.providers).forEach((key) => { const provider = this.authConfig.oAuth.providers[key]; const callbackPath = `/auth/oAuthCallback/${key}`; const serverApiAddress = this.authConfig.oAuth.serverApiAddress; const callbackURL = serverApiAddress + callbackPath; const providerConfig = { ...provider.config, callbackURL }; const providerOptions = { scope: ["email"], ...provider.options, session: false }; passport.use( new provider.strategy(providerConfig, async (accessToken, refreshToken, profile, cb) => { const queryRunner = this.orm.createQueryRunner(); try { let email = profile.email || profile._json.email; if (email == null && profile.emails != null && profile.emails[0] != null && profile.emails[0].value != null) { email = profile.emails[0].value; } if (profile == null || email == null || profile.id == null) { throw new Error("NotificationEmail or id is missing!"); } const oAuthAuthProvider = this.oAuthAuthProviders[key]; const authConnector = oAuthAuthProvider.getAuthConnector(); await queryRunner.connect(); await queryRunner.startTransaction(); let user = await authConnector.findUser(queryRunner, email, provider.tenant); if (user.isFake === true) { user = await authConnector.findUser(queryRunner, profile.id, provider.tenant); } if (user.isFake === true) { const oAuthAuthFactorCreationToken = await oAuthAuthProvider.create(profile.id, null, true, { oAuthProvider: key }); const emailAuthFactorCreationToken = await this.emailAuthProvider.create(email, email, true, { oAuthProvider: key }); const response = { email, profile, authFactorCreationTokens: { oAuth: oAuthAuthFactorCreationToken, email: emailAuthFactorCreationToken } }; await queryRunner.commitTransaction(); cb(null, response); } else { let emailAuthFactorProof = await this.emailAuthProvider.proof(queryRunner, user.userIdentifier, async (authFactor) => { return email; }); let oAuthAuthFactorProof = await oAuthAuthProvider.proof(queryRunner, user.userIdentifier, async (authFactor) => { return profile.id; }); if (emailAuthFactorProof.isFake === true && oAuthAuthFactorProof.isFake === true) { throw new Error("All AuthFactors are invalid."); } let authFactorCreationToken; let authFactorProofToken; if (emailAuthFactorProof.isFake === true) { authFactorProofToken = oAuthAuthFactorProof.authFactorProofToken; authFactorCreationToken = await this.emailAuthProvider.create(email, email, true, { oAuthProvider: key }); } if (oAuthAuthFactorProof.isFake === true) { authFactorProofToken = emailAuthFactorProof.authFactorProofToken; authFactorCreationToken = await oAuthAuthProvider.create(profile.id, null, true, { oAuthProvider: key }); } const userAuthentication = await authConnector.getUserAuthenticationById(queryRunner, user.userAuthenticationId); const loginProviderSets = userAuthentication.loginProviderSets; if (loginProviderSets.indexOf(key) < 0) { loginProviderSets.push(key); } if (authFactorCreationToken != null) { await authConnector.modifyAuthFactors( queryRunner, [authFactorProofToken], true, loginProviderSets, null, [authFactorCreationToken], [] ); if (emailAuthFactorProof.isFake === true) { emailAuthFactorProof = await this.emailAuthProvider.proof(queryRunner, user.userIdentifier, async (authFactor) => { return email; }); } else { oAuthAuthFactorProof = await oAuthAuthProvider.proof(queryRunner, user.userIdentifier, async (authFactor) => { return profile.id; }); } } const response = { email, profile, authFactorProofTokens: { email: emailAuthFactorProof.authFactorProofToken, oAuth: oAuthAuthFactorProof.authFactorProofToken } }; await queryRunner.commitTransaction(); cb(null, response); } } catch (err) { try { await queryRunner.rollbackTransaction(); } catch (e) { /* don't care */ } this.logger.warn("passport.strategylogin.error", err); cb(err); } finally { queryRunner.release(); } }) ); authRouter.get( `/auth/oAuth/${key}`, (ctx, next) => { const { queryParameter } = this.authConfig.privacyAgreementAcceptance; next(); }, passport.authenticate(key, providerOptions) ); const errorCatcher = async (ctx, next) => { try { await next(); } catch (err) { this.logger.warn("passport.oAuthFailure.error", err); ctx.redirect("/auth/oAuthFailure"); } }; authRouter.get(callbackPath, errorCatcher, passport.authenticate(key, { failureRedirect: "/auth/oAuthFailure", session: false }), (ctx) => { ctx.redirect(`/auth/oAuthSuccess/${encodeURIComponent(JSON.stringify(ctx.state.user))}`); }); }); app.use(authRouter.routes()); app.use(authRouter.allowedMethods()); } }