├── image-1.png ├── image.png ├── tsconfig.build.json ├── src ├── auth │ ├── interfaces │ │ └── UserDetails.ts │ ├── entities │ │ └── user.entity.ts │ ├── utils │ │ ├── guards │ │ │ └── guards.ts │ │ ├── serializer.ts │ │ └── GoogleStrategy.ts │ ├── auth.controller.ts │ ├── auth.module.ts │ └── auth.service.ts ├── main.ts └── app.module.ts ├── nest-cli.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .env.example ├── docker-compose.yml ├── tsconfig.json ├── .gitignore ├── package.json └── README.md /image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mateo-Alvarez1/OAuth/HEAD/image-1.png -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mateo-Alvarez1/OAuth/HEAD/image.png -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/interfaces/UserDetails.ts: -------------------------------------------------------------------------------- 1 | export interface userDetails { 2 | fullname: string; 3 | email: string; 4 | accessToken: string; 5 | refreshToken: string; 6 | } 7 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_CLIENT_ID=your_google_client_id 2 | GOOGLE_CLIENT_SECRET=your_google_client_secret 3 | DB_PASSWORD=your_db_password 4 | DB_NAME=your_db_name 5 | DB_USERNAME=your_db_username 6 | DB_PORT=5432 7 | DB_HOST=localhost -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: "postgres:14.3" 6 | restart: always 7 | ports: 8 | - "5432:5432" 9 | environment: 10 | POSTGRES_USER: ${DB_USERNAME} 11 | POSTGRES_PASSWORD: ${DB_PASSWORD} 12 | POSTGRES_DB: ${DB_NAME} 13 | volumes: 14 | - ./postgres:/var/lib/postgres/data 15 | -------------------------------------------------------------------------------- /src/auth/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; 2 | 3 | @Entity() 4 | export class User { 5 | @PrimaryGeneratedColumn("uuid") 6 | id: string; 7 | @Column("text") 8 | fullname: string; 9 | @Column("text") 10 | email: string; 11 | @Column("text") 12 | accessToken: string; 13 | @Column("text") 14 | refreshToken: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/auth/utils/guards/guards.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from "@nestjs/common"; 2 | import { AuthGuard } from "@nestjs/passport"; 3 | 4 | @Injectable() 5 | export class GoogleAuthGuard extends AuthGuard("google") { 6 | async canActivate(context: ExecutionContext) { 7 | const activate = (await super.canActivate(context)) as boolean; 8 | const request = context.switchToHttp().getRequest(); 9 | await super.logIn(request); 10 | return activate; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards } from "@nestjs/common"; 2 | import { AuthService } from "./auth.service"; 3 | import { GoogleAuthGuard } from "./utils/guards/guards"; 4 | 5 | @Controller("auth") 6 | export class AuthController { 7 | constructor(private readonly authService: AuthService) {} 8 | 9 | @Get("google/login") 10 | @UseGuards(GoogleAuthGuard) 11 | login() { 12 | return { msg: "GoogleAuth" }; 13 | } 14 | 15 | @Get("google/redirect") 16 | @UseGuards(GoogleAuthGuard) 17 | redirect() { 18 | return { 19 | msg: "ok", 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "./app.module"; 3 | import * as session from "express-session"; 4 | import * as passport from "passport"; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.setGlobalPrefix("api"); 9 | app.use( 10 | session({ 11 | secret: "asiodasjoddjdoasddasoidjasiodasdjaiodd", 12 | saveUninitialized: false, 13 | resave: false, 14 | cookie: { 15 | maxAge: 60000, 16 | }, 17 | }) 18 | ); 19 | app.use(passport.initialize()); 20 | app.use(passport.session()); 21 | await app.listen(process.env.PORT ?? 3000); 22 | } 23 | bootstrap(); 24 | -------------------------------------------------------------------------------- /src/auth/utils/serializer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { PassportSerializer } from "@nestjs/passport"; 3 | import { AuthService } from "../auth.service"; 4 | import { User } from "../entities/user.entity"; 5 | 6 | @Injectable() 7 | export class SessionSerializer extends PassportSerializer { 8 | constructor( 9 | @Inject("AUTH_SERVICE") private readonly authService: AuthService 10 | ) { 11 | super(); 12 | } 13 | 14 | serializeUser(user: User, done: Function) { 15 | done(null, user); 16 | } 17 | 18 | async deserializeUser(payload: any, done: Function) { 19 | const user = await this.authService.findUser(payload.id); 20 | return user ? done(null, user) : done(null, null); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { AuthService } from "./auth.service"; 3 | import { AuthController } from "./auth.controller"; 4 | import { ConfigModule } from "@nestjs/config"; 5 | import { GoogleStrategy } from "./utils/GoogleStrategy"; 6 | import { TypeOrmModule } from "@nestjs/typeorm"; 7 | import { User } from "./entities/user.entity"; 8 | import { SessionSerializer } from "./utils/serializer"; 9 | 10 | @Module({ 11 | controllers: [AuthController], 12 | providers: [ 13 | SessionSerializer, 14 | AuthService, 15 | GoogleStrategy, 16 | { 17 | provide: "AUTH_SERVICE", 18 | useClass: AuthService, 19 | }, 20 | ], 21 | imports: [ConfigModule, TypeOrmModule.forFeature([User])], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { App } from 'supertest/types'; 5 | import { AppModule } from './../src/app.module'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | await app.init(); 17 | }); 18 | 19 | it('/ (GET)', () => { 20 | return request(app.getHttpServer()) 21 | .get('/') 22 | .expect(200) 23 | .expect('Hello World!'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { AuthModule } from "./auth/auth.module"; 3 | import { ConfigModule } from "@nestjs/config"; 4 | import { TypeOrmModule } from "@nestjs/typeorm"; 5 | import { PassportModule } from "@nestjs/passport"; 6 | import { session } from "passport"; 7 | 8 | @Module({ 9 | imports: [ 10 | PassportModule.register({ session: true }), 11 | AuthModule, 12 | ConfigModule.forRoot(), 13 | TypeOrmModule.forRoot({ 14 | type: "postgres", 15 | host: process.env.DB_HOST, 16 | port: Number(process.env.DB_PORT), 17 | database: process.env.DB_NAME, 18 | username: process.env.DB_USERNAME, 19 | password: process.env.DB_PASSWORD, 20 | autoLoadEntities: true, 21 | synchronize: true, 22 | }), 23 | ], 24 | controllers: [], 25 | providers: [], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { InjectRepository } from "@nestjs/typeorm"; 3 | import { User } from "./entities/user.entity"; 4 | import { Repository } from "typeorm"; 5 | import { userDetails } from "./interfaces/UserDetails"; 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor( 10 | @InjectRepository(User) 11 | private readonly userRepository: Repository 12 | ) {} 13 | 14 | async validateUser(details: userDetails) { 15 | console.log(" AuthService - details", details); 16 | 17 | const user = await this.userRepository.findOneBy({ email: details.email }); 18 | 19 | if (user) { 20 | return user; 21 | } 22 | 23 | const newUser = this.userRepository.create(details); 24 | return this.userRepository.save(newUser); 25 | } 26 | 27 | async findUser(id: string) { 28 | const user = await this.userRepository.findOneBy({ id }); 29 | return user; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/utils/GoogleStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { PassportStrategy } from "@nestjs/passport"; 4 | import { Profile } from "passport"; 5 | import { Strategy } from "passport-google-oauth20"; 6 | import { AuthService } from "../auth.service"; 7 | 8 | @Injectable() 9 | export class GoogleStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | @Inject("AUTH_SERVICE") 12 | private readonly authService: AuthService, 13 | private readonly configService: ConfigService 14 | ) { 15 | super({ 16 | clientID: String(configService.get("GOOGLE_CLIENT_ID")), 17 | clientSecret: String(configService.get("GOOGLE_CLIENT_SECRET")), 18 | callbackURL: "http://localhost:3000/api/auth/google/redirect", 19 | scope: ["profile", "email"], 20 | }); 21 | } 22 | 23 | authorizationParams(options: any) { 24 | return { 25 | access_type: "offline", 26 | prompt: "consent", 27 | }; 28 | } 29 | 30 | async validate(accessToken: string, refreshToken: string, profile: Profile) { 31 | console.log(" GoogleStrategy - validate - profile", profile); 32 | console.log(" GoogleStrategy - validate - refreshToken", refreshToken); 33 | console.log(" GoogleStrategy - validate - accessToken", accessToken); 34 | const email = profile.emails?.[0]?.value; 35 | 36 | if (!email) { 37 | throw new Error("Email not found in Google profile"); 38 | } 39 | 40 | const user = await this.authService.validateUser({ 41 | email, 42 | fullname: profile.displayName, 43 | accessToken: accessToken, 44 | refreshToken: refreshToken, 45 | }); 46 | 47 | return user! || null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@nestjs/common": "^11.0.1", 23 | "@nestjs/config": "^4.0.2", 24 | "@nestjs/core": "^11.0.1", 25 | "@nestjs/mapped-types": "*", 26 | "@nestjs/passport": "^11.0.5", 27 | "@nestjs/platform-express": "^11.0.1", 28 | "@nestjs/typeorm": "^11.0.0", 29 | "express-session": "^1.18.1", 30 | "passport": "^0.7.0", 31 | "passport-google-oauth20": "^2.0.0", 32 | "pg": "^8.16.2", 33 | "reflect-metadata": "^0.2.2", 34 | "rxjs": "^7.8.1", 35 | "typeorm": "^0.3.25" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^11.0.0", 39 | "@nestjs/schematics": "^11.0.0", 40 | "@nestjs/testing": "^11.0.1", 41 | "@swc/cli": "^0.6.0", 42 | "@swc/core": "^1.10.7", 43 | "@types/express": "^5.0.0", 44 | "@types/express-session": "^1.18.2", 45 | "@types/jest": "^29.5.14", 46 | "@types/node": "^22.10.7", 47 | "@types/passport-google-oauth20": "^2.0.16", 48 | "@types/supertest": "^6.0.2", 49 | "globals": "^16.0.0", 50 | "jest": "^29.7.0", 51 | "source-map-support": "^0.5.21", 52 | "supertest": "^7.0.0", 53 | "ts-jest": "^29.2.5", 54 | "ts-loader": "^9.5.2", 55 | "ts-node": "^10.9.2", 56 | "tsconfig-paths": "^4.2.0", 57 | "typescript": "^5.7.3" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".*\\.spec\\.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "collectCoverageFrom": [ 71 | "**/*.(t|j)s" 72 | ], 73 | "coverageDirectory": "../coverage", 74 | "testEnvironment": "node" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google OAuth2 2 | 3 | **Si llegaste desde TikTok, primero que nada , gracias por bancar mi contenido . Segundo si sos bueno me ayuda mucho que me sigas y tercero pero no menos importante,Si comentas `"GITHUBAUTH"` en el video vas a estar ayudando para que saque un video implementando Autenticacion con Github . Ahora si vamos a lo importante** 4 | 5 | --- 6 | 7 | ### Antes de pegar este codigo en tu editor seguí estos pasos por que sino no te va a funcar nada :) 8 | 9 | 1. **Clonar Proyecto** 10 | 11 | 2. **Instalar Dependecias** 12 | `npm install` 13 | 3. **Clonar el archivo `.env.template` y renombrarlo a `.env`** 14 | 4. **Cambiar las variables de entorno** 15 | 16 | ## Configuracion Google Console 17 | 18 | 1. **Anda a Google Console y crear una nueva App** 19 | 20 | ![alt text](image-1.png) 21 | 22 | 2.**Una cez que tenes Creada tu App volves a la pestaña de incio e ingresas a la misma** 23 | ![imagen](https://github.com/user-attachments/assets/638a7789-ddcb-4e40-b7b7-ec78e4d6aae3) 24 | 25 | 3. **Una vez que ingresaste vas a poner en el buscador 'Credentials'** 26 | 27 | ![imagen](https://github.com/user-attachments/assets/f8917148-a359-4b68-a19d-f872ff499a29) 28 | 29 | 5. **Vas a configurar tu aplicacion con los datos que te pida** 30 | ![imagen](https://github.com/user-attachments/assets/9b99c308-7aac-4b39-a401-9ba7dd645ba7) 31 | 32 | 6. **Agregas un usuario de prueba , te recomiendo que pongas tu email** 33 | ![imagen](https://github.com/user-attachments/assets/d1b2205a-69fe-459d-a5de-05c5ba4707aa) 34 | 35 | 7. **Volves a la pestaña principal y le das en Crear Credenciales**** 36 | 37 | ![imagen](https://github.com/user-attachments/assets/cb0bbff7-688e-4b3f-bd90-f2e648ce1d11) 38 | 39 | 8. **Seleccionas la opcion de : ID de cliente OAuth** 40 | 41 | ![imagen](https://github.com/user-attachments/assets/26dddd74-bef9-4305-984c-dbfbcdb643de) 42 | 43 | 9. **Seleccionas > Aplicacion Web > Configuras la URI de redireccion (En este caso usamos local ya que estamos en desarrollo)** 44 | 45 | ![imagen](https://github.com/user-attachments/assets/ba3eb4d5-5e34-4a65-899c-422d9bffc4e2) 46 | 47 | 10. **Le damos a aceptar y tenemos nuestos Client ID y Client Secret Ready** 48 | 49 | ![imagen](https://github.com/user-attachments/assets/f631ea21-f778-41e0-86db-2c86f41b36fa) 50 | 51 | ## ⭐ ¿Te sirvió? 52 | Seguime en tiktok que subo contenido sobre desarrollo , automatizaciones y experiencias 53 | 👉 **@matualvarez_** 54 | --------------------------------------------------------------------------------