Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Google auth provider #15

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading