feat(auth): implement Google and Zalo OAuth backend strategies

Add complete OAuth2 authentication flow for Google and Zalo providers:
- OAuthService: handles account linking (by email/phone), new user
  creation for OAuth-only accounts, and JWT token generation
- GoogleOAuthStrategy: passport-google-oauth20 integration
- ZaloOAuthStrategy: custom OAuth2 implementation using Zalo's API
  (authorization URL generation, code exchange, user info fetch)
- OAuthController: redirect and callback endpoints for both providers
  with httpOnly cookie-based token management
- Unit tests for OAuthService (7 tests), GoogleOAuthStrategy (4 tests),
  and ZaloOAuthStrategy (7 tests)
- OAuth env vars added to .env.example and env-validation warnings

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 14:14:02 +07:00
parent bac3313873
commit 23bb380d34
14 changed files with 1068 additions and 2 deletions

67
pnpm-lock.yaml generated
View File

@@ -150,6 +150,9 @@ importers:
passport:
specifier: ^0.7.0
version: 0.7.0
passport-google-oauth20:
specifier: ^2.0.0
version: 2.0.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
@@ -208,6 +211,9 @@ importers:
'@types/nodemailer':
specifier: ^8.0.0
version: 8.0.0
'@types/passport-google-oauth20':
specifier: ^2.0.17
version: 2.0.17
'@types/passport-jwt':
specifier: ^4.0.1
version: 4.0.1
@@ -2440,12 +2446,21 @@ packages:
'@types/nodemailer@8.0.0':
resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==}
'@types/oauth@0.9.6':
resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==}
'@types/passport-google-oauth20@2.0.17':
resolution: {integrity: sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==}
'@types/passport-jwt@4.0.1':
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
'@types/passport-local@1.0.38':
resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==}
'@types/passport-oauth2@1.8.0':
resolution: {integrity: sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==}
'@types/passport-strategy@0.2.38':
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
@@ -2926,6 +2941,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
base64url@3.0.1:
resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==}
engines: {node: '>=6.0.0'}
baseline-browser-mapping@2.10.16:
resolution: {integrity: sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==}
engines: {node: '>=6.0.0'}
@@ -4529,6 +4548,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
oauth@0.10.2:
resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -4597,6 +4619,10 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
passport-google-oauth20@2.0.0:
resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==}
engines: {node: '>= 0.4.0'}
passport-jwt@4.0.1:
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
@@ -4604,6 +4630,10 @@ packages:
resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==}
engines: {node: '>= 0.4.0'}
passport-oauth2@1.8.0:
resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==}
engines: {node: '>= 0.4.0'}
passport-strategy@1.0.0:
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
engines: {node: '>= 0.4.0'}
@@ -5520,6 +5550,9 @@ packages:
engines: {node: '>=0.8.0'}
hasBin: true
uid2@0.0.4:
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
uid@2.0.2:
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
engines: {node: '>=8'}
@@ -8441,6 +8474,16 @@ snapshots:
dependencies:
'@types/node': 25.5.2
'@types/oauth@0.9.6':
dependencies:
'@types/node': 25.5.2
'@types/passport-google-oauth20@2.0.17':
dependencies:
'@types/express': 5.0.6
'@types/passport': 1.0.17
'@types/passport-oauth2': 1.8.0
'@types/passport-jwt@4.0.1':
dependencies:
'@types/jsonwebtoken': 9.0.10
@@ -8452,6 +8495,12 @@ snapshots:
'@types/passport': 1.0.17
'@types/passport-strategy': 0.2.38
'@types/passport-oauth2@1.8.0':
dependencies:
'@types/express': 5.0.6
'@types/oauth': 0.9.6
'@types/passport': 1.0.17
'@types/passport-strategy@0.2.38':
dependencies:
'@types/express': 5.0.6
@@ -8965,6 +9014,8 @@ snapshots:
base64-js@1.5.1: {}
base64url@3.0.1: {}
baseline-browser-mapping@2.10.16: {}
bcrypt@6.0.0:
@@ -10654,6 +10705,8 @@ snapshots:
pathe: 2.0.3
tinyexec: 1.1.1
oauth@0.10.2: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
@@ -10726,6 +10779,10 @@ snapshots:
parseurl@1.3.3: {}
passport-google-oauth20@2.0.0:
dependencies:
passport-oauth2: 1.8.0
passport-jwt@4.0.1:
dependencies:
jsonwebtoken: 9.0.3
@@ -10735,6 +10792,14 @@ snapshots:
dependencies:
passport-strategy: 1.0.0
passport-oauth2@1.8.0:
dependencies:
base64url: 3.0.1
oauth: 0.10.2
passport-strategy: 1.0.0
uid2: 0.0.4
utils-merge: 1.0.1
passport-strategy@1.0.0: {}
passport@0.7.0:
@@ -11719,6 +11784,8 @@ snapshots:
uglify-js@3.19.3:
optional: true
uid2@0.0.4: {}
uid@2.0.2:
dependencies:
'@lukeed/csprng': 1.1.0