Skip to content

Commit

Permalink
feat: add Google auth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Jan 9, 2024
1 parent 76e7b3f commit 11f174a
Show file tree
Hide file tree
Showing 38 changed files with 370 additions and 152 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ API_URL=http://localhost:3000/api
#AUTH_GITHUB_CLIENT_SECRET=
# Enable login with GitHub
#AUTH_GITHUB_ENABLED=true
# Google Admin IDs (comma-separated)
#AUTH_GOOGLE_ADMIN_IDS=
# Google OAuth
#AUTH_GOOGLE_CLIENT_ID=
#AUTH_GOOGLE_CLIENT_SECRET=
# Enable login with Google
#AUTH_GOOGLE_ENABLED=true
# Twitter Admin IDs (comma-separated)
#AUTH_TWITTER_ADMIN_IDS=386584531353862154
# Twitter OAuth2 Consumer Key and Consumer Secret
Expand Down
2 changes: 2 additions & 0 deletions api-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ input AdminUpdateUserInput {
type AppConfig {
authDiscordEnabled: Boolean!
authGithubEnabled: Boolean!
authGoogleEnabled: Boolean!
authPasswordEnabled: Boolean!
authRegisterEnabled: Boolean!
authSolanaEnabled: Boolean!
Expand Down Expand Up @@ -80,6 +81,7 @@ type IdentityChallenge {
enum IdentityProvider {
Discord
GitHub
Google
Solana
Twitter
}
Expand Down
1 change: 1 addition & 0 deletions libs/api/auth/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export * from './lib/guards/api-auth-graphql-user-guard'
export * from './lib/interfaces/api-auth.request'
export * from './lib/strategies/oauth/api-auth-strategy-discord-guard'
export * from './lib/strategies/oauth/api-auth-strategy-github-guard'
export * from './lib/strategies/oauth/api-auth-strategy-google-guard'
export * from './lib/strategies/oauth/api-auth-strategy-twitter-guard'
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type DynamicModule, Module } from '@nestjs/common'

import { ApiAuthStrategyDiscordModule } from './oauth/api-auth-strategy-discord.module'
import { ApiAuthStrategyGithubModule } from './oauth/api-auth-strategy-github.module'
import { ApiAuthStrategyGoogleModule } from './oauth/api-auth-strategy-google.module'
import { ApiAuthStrategyTwitterModule } from './oauth/api-auth-strategy-twitter.module'

@Module({})
Expand All @@ -12,6 +13,7 @@ export class ApiAuthStrategyModule {
imports: [
ApiAuthStrategyDiscordModule.register(),
ApiAuthStrategyGithubModule.register(),
ApiAuthStrategyGoogleModule.register(),
ApiAuthStrategyTwitterModule.register(),
],
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class ApiAuthStrategyGoogleGuard extends AuthGuard('google') {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type DynamicModule, Logger, Module } from '@nestjs/common'
import { ApiCoreDataAccessModule } from '@pubkey-stack/api-core-data-access'
import { ApiAuthStrategyService } from '../api-auth-strategy.service'
import { ApiAuthStrategyGoogle } from './api-auth-strategy-google'

@Module({})
export class ApiAuthStrategyGoogleModule {
static logger = new Logger(ApiAuthStrategyGoogleModule.name)
static register(): DynamicModule {
const enabled = this.enabled
if (!enabled) {
this.logger.warn(`Google Auth DISABLED`)
return { module: ApiAuthStrategyGoogleModule }
}
this.logger.verbose(`Google Auth ENABLED`)
return {
module: ApiAuthStrategyGoogleModule,
imports: [ApiCoreDataAccessModule],
providers: [ApiAuthStrategyGoogle, ApiAuthStrategyService],
}
}

// TODO: These should be coming from the ApiCoreConfigService instead of process.env
private static get enabled(): boolean {
return (
// Google auth needs to be enabled
!!process.env['AUTH_GOOGLE_ENABLED'] &&
// And we need to have the client ID and secret set
!!process.env['AUTH_GOOGLE_CLIENT_ID'] &&
!!process.env['AUTH_GOOGLE_CLIENT_SECRET']
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { IdentityProvider } from '@prisma/client'
import { ApiCoreService } from '@pubkey-stack/api-core-data-access'
import { Profile, Strategy } from 'passport-google-oauth20'
import type { ApiAuthRequest } from '../../interfaces/api-auth.request'
import { ApiAuthStrategyService } from '../api-auth-strategy.service'

@Injectable()
export class ApiAuthStrategyGoogle extends PassportStrategy(Strategy, 'google') {
constructor(private core: ApiCoreService, private service: ApiAuthStrategyService) {
super(core.config.authGoogleStrategyOptions)
}

async validate(req: ApiAuthRequest, accessToken: string, refreshToken: string, profile: Profile) {
return this.service.validateRequest({
req,
providerId: profile.id,
provider: IdentityProvider.Google,
accessToken,
refreshToken,
profile: createGoogleProfile(profile),
})
}
}

function createGoogleProfile(profile: Profile) {
return {
externalId: profile.id,
username: profile.username,
avatarUrl: profile.photos?.[0].value,
name: profile.displayName,
}
}
2 changes: 2 additions & 0 deletions libs/api/auth/feature/src/lib/api-auth-feature.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'
import { ApiAuthDataAccessModule } from '@pubkey-stack/api-auth-data-access'
import { ApiAuthStrategyDiscordController } from './api-auth-strategy-discord.controller'
import { ApiAuthStrategyGithubController } from './api-auth-strategy-github.controller'
import { ApiAuthStrategyGoogleController } from './api-auth-strategy-google.controller'
import { ApiAuthStrategyTwitterController } from './api-auth-strategy-twitter.controller'
import { ApiAuthController } from './api-auth.controller'
import { ApiAuthResolver } from './api-auth.resolver'
Expand All @@ -11,6 +12,7 @@ import { ApiAuthResolver } from './api-auth.resolver'
ApiAuthController,
ApiAuthStrategyDiscordController,
ApiAuthStrategyGithubController,
ApiAuthStrategyGoogleController,
ApiAuthStrategyTwitterController,
],
imports: [ApiAuthDataAccessModule],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'

import {
ApiAnonJwtGuard,
ApiAuthRequest,
ApiAuthService,
ApiAuthStrategyGoogleGuard,
} from '@pubkey-stack/api-auth-data-access'
import { Response } from 'express-serve-static-core'

@Controller('auth/google')
export class ApiAuthStrategyGoogleController {
constructor(private readonly service: ApiAuthService) {}

@Get()
@UseGuards(ApiAuthStrategyGoogleGuard)
redirect() {
// This method triggers the OAuth2 flow
}

@Get('callback')
@UseGuards(ApiAnonJwtGuard, ApiAuthStrategyGoogleGuard)
async callback(@Req() req: ApiAuthRequest, @Res({ passthrough: true }) res: Response) {
return this.service.userCookieRedirect(req, res)
}
}
38 changes: 38 additions & 0 deletions libs/api/core/data-access/src/lib/api-core-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ApiCoreConfigService {
return {
authDiscordEnabled: this.authDiscordEnabled,
authGithubEnabled: this.authGithubEnabled,
authGoogleEnabled: this.authGoogleEnabled,
authPasswordEnabled: this.authPasswordEnabled,
authRegisterEnabled: this.authRegisterEnabled,
authSolanaEnabled: this.authSolanaEnabled,
Expand Down Expand Up @@ -92,6 +93,41 @@ export class ApiCoreConfigService {
!this.service.get<boolean>('authGithubEnabled')
)
}

get authGoogleAdminIds() {
return this.service.get<string[]>('authGoogleAdminIds')
}

get authGoogleClientId() {
return this.service.get<string>('authGoogleClientId')
}

get authGoogleClientSecret() {
return this.service.get<string>('authGoogleClientSecret')
}

get authGoogleScope(): string[] {
return ['email', 'profile']
}

get authGoogleStrategyOptions() {
return {
clientID: this.authGoogleClientId,
clientSecret: this.authGoogleClientSecret,
callbackURL: this.webUrl + '/api/auth/google/callback',
scope: this.authGoogleScope,
passReqToCallback: true,
}
}

get authGoogleEnabled(): boolean {
return !(
!this.authGoogleClientId ||
!this.authGoogleClientSecret ||
!this.service.get<boolean>('authGoogleEnabled')
)
}

get authTwitterAdminIds() {
return this.service.get<string[]>('authTwitterAdminIds')
}
Expand Down Expand Up @@ -212,6 +248,8 @@ export class ApiCoreConfigService {
return this.authDiscordAdminIds?.includes(providerId) ?? false
case IdentityProvider.GitHub:
return this.authGithubAdminIds?.includes(providerId) ?? false
case IdentityProvider.Google:
return this.authGoogleAdminIds?.includes(providerId) ?? false
case IdentityProvider.Solana:
return this.authSolanaAdminIds?.includes(providerId) ?? false
case IdentityProvider.Twitter:
Expand Down
9 changes: 9 additions & 0 deletions libs/api/core/data-access/src/lib/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export interface ApiCoreConfig {
authGithubClientId: string
authGithubClientSecret: string
authGithubEnabled: boolean
// Google Authentication
authGoogleAdminIds: string[]
authGoogleClientId: string
authGoogleClientSecret: string
authGoogleEnabled: boolean
// Twitter Authentication
authTwitterAdminIds: string[]
authTwitterConsumerKey: string
Expand Down Expand Up @@ -71,6 +76,10 @@ export function configuration(): ApiCoreConfig {
authGithubClientId: process.env['AUTH_GITHUB_CLIENT_ID'] as string,
authGithubClientSecret: process.env['AUTH_GITHUB_CLIENT_SECRET'] as string,
authGithubEnabled: process.env['AUTH_GITHUB_ENABLED'] === 'true',
authGoogleAdminIds: getFromEnvironment('AUTH_GOOGLE_ADMIN_IDS'),
authGoogleClientId: process.env['AUTH_GOOGLE_CLIENT_ID'] as string,
authGoogleClientSecret: process.env['AUTH_GOOGLE_CLIENT_SECRET'] as string,
authGoogleEnabled: process.env['AUTH_GOOGLE_ENABLED'] === 'true',
authTwitterAdminIds: getFromEnvironment('AUTH_TWITTER_ADMIN_IDS'),
authTwitterConsumerKey: process.env['AUTH_TWITTER_CONSUMER_KEY'] as string,
authTwitterConsumerSecret: process.env['AUTH_TWITTER_CONSUMER_SECRET'] as string,
Expand Down
13 changes: 9 additions & 4 deletions libs/api/core/data-access/src/lib/config/validation-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ export const validationSchema = Joi.object({
AUTH_DISCORD_ADMIN_IDS: Joi.string(),
AUTH_DISCORD_CLIENT_ID: Joi.string(),
AUTH_DISCORD_CLIENT_SECRET: Joi.string(),
AUTH_DISCORD_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required
AUTH_DISCORD_ENABLED: Joi.boolean().default(true),
// GitHub Authentication
AUTH_GITHUB_ADMIN_IDS: Joi.string(),
AUTH_GITHUB_CLIENT_ID: Joi.string(),
AUTH_GITHUB_CLIENT_SECRET: Joi.string(),
AUTH_GITHUB_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required
// GitHub Authentication
AUTH_GITHUB_ENABLED: Joi.boolean().default(true),
// Google Authentication
AUTH_GOOGLE_ADMIN_IDS: Joi.string(),
AUTH_GOOGLE_CLIENT_ID: Joi.string(),
AUTH_GOOGLE_CLIENT_SECRET: Joi.string(),
AUTH_GOOGLE_ENABLED: Joi.boolean().default(true),
// Twitter Authentication
AUTH_TWITTER_ADMIN_IDS: Joi.string(),
AUTH_TWITTER_CONSUMER_KEY: Joi.string(),
AUTH_TWITTER_CONSUMER_SECRET: Joi.string(),
AUTH_TWITTER_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required
AUTH_TWITTER_ENABLED: Joi.boolean().default(true),
// Username and Password Authentication
AUTH_PASSWORD_ENABLED: Joi.boolean().default(true),
AUTH_REGISTER_ENABLED: Joi.boolean().default(true),
Expand Down
2 changes: 2 additions & 0 deletions libs/api/core/data-access/src/lib/entity/app-config.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export class AppConfig {
@Field()
authGithubEnabled!: boolean
@Field()
authGoogleEnabled!: boolean
@Field()
authPasswordEnabled!: boolean
@Field()
authRegisterEnabled!: boolean
Expand Down
5 changes: 5 additions & 0 deletions libs/sdk/src/generated/graphql-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type AppConfig = {
__typename?: 'AppConfig'
authDiscordEnabled: Scalars['Boolean']['output']
authGithubEnabled: Scalars['Boolean']['output']
authGoogleEnabled: Scalars['Boolean']['output']
authPasswordEnabled: Scalars['Boolean']['output']
authRegisterEnabled: Scalars['Boolean']['output']
authSolanaEnabled: Scalars['Boolean']['output']
Expand Down Expand Up @@ -100,6 +101,7 @@ export type IdentityChallenge = {
export enum IdentityProvider {
Discord = 'Discord',
GitHub = 'GitHub',
Google = 'Google',
Solana = 'Solana',
Twitter = 'Twitter',
}
Expand Down Expand Up @@ -373,6 +375,7 @@ export type AppConfigDetailsFragment = {
__typename?: 'AppConfig'
authDiscordEnabled: boolean
authGithubEnabled: boolean
authGoogleEnabled: boolean
authPasswordEnabled: boolean
authRegisterEnabled: boolean
authSolanaEnabled: boolean
Expand Down Expand Up @@ -402,6 +405,7 @@ export type AppConfigQuery = {
__typename?: 'AppConfig'
authDiscordEnabled: boolean
authGithubEnabled: boolean
authGoogleEnabled: boolean
authPasswordEnabled: boolean
authRegisterEnabled: boolean
authSolanaEnabled: boolean
Expand Down Expand Up @@ -854,6 +858,7 @@ export const AppConfigDetailsFragmentDoc = gql`
fragment AppConfigDetails on AppConfig {
authDiscordEnabled
authGithubEnabled
authGoogleEnabled
authPasswordEnabled
authRegisterEnabled
authSolanaEnabled
Expand Down
1 change: 1 addition & 0 deletions libs/sdk/src/graphql/feature-core.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
fragment AppConfigDetails on AppConfig {
authDiscordEnabled
authGithubEnabled
authGoogleEnabled
authPasswordEnabled
authRegisterEnabled
authSolanaEnabled
Expand Down
Loading

0 comments on commit 11f174a

Please sign in to comment.