Chuyển đến nội dung chính

レッスン 12: セッション、Cookie、OAuth2

セッション管理、Cookie ベースの認証、Google/GitHub による OAuth2。ソーシャルログイン統合、アカウントリンク。 OpenID Connect の基本。

💻 プログラミング — レッスン 12 レッスン 12: セッション、Cookie、OAuth2

NestJS: 基本から高度まで

パート 3: 認証とセキュリティ

xdev.asia

1. セッション管理

npm install express-session @types/express-session connect-redis
// main.ts
import * as session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000, // 1 ngày
  },
}));
// Sử dụng session trong controller
@Controller('auth')
export class AuthController {
  @Post('login')
  login(@Body() dto: LoginDto, @Session() session: Record<string, any>) {
    const user = this.authService.validate(dto);
    session.userId = user.id;
    session.role = user.role;
    return { message: 'Logged in' };
  }

  @Post('logout')
  logout(@Session() session: Record<string, any>) {
    session.destroy();
    return { message: 'Logged out' };
  }
}

2. Cookie ベースの認証

import { Response } from 'express';

@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const { accessToken, refreshToken } = await this.authService.login(dto);
  
  // Set access token trong HttpOnly cookie
  res.cookie('access_token', accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 15 * 60 * 1000,  // 15 phút
  });

  // Refresh token trong separate cookie
  res.cookie('refresh_token', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/auth/refresh',  // Chỉ gửi khi refresh
    maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 ngày
  });

  return { message: 'Logged in' };
}

// JWT Strategy đọc từ cookie
@Injectable()
export class JwtCookieStrategy extends PassportStrategy(Strategy, 'jwt-cookie') {
  constructor() {
    super({
      jwtFromRequest: (req: Request) => req.cookies?.access_token,
      secretOrKey: process.env.JWT_ACCESS_SECRET,
    });
  }

  validate(payload: JwtPayload) {
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

3. OAuth2 — Google ログイン

npm install passport-google-oauth20
npm install -D @types/passport-google-oauth20
// auth/strategies/google.strategy.ts
import { Strategy, VerifyCallback } from 'passport-google-oauth20';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private config: ConfigService) {
    super({
      clientID: config.get('GOOGLE_CLIENT_ID'),
      clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
      callbackURL: config.get('GOOGLE_CALLBACK_URL'),
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ) {
    const { name, emails, photos } = profile;
    const user = {
      email: emails[0].value,
      name: `${name.givenName} ${name.familyName}`,
      avatar: photos[0]?.value,
      provider: 'google',
      providerId: profile.id,
    };
    done(null, user);
  }
}
// auth/auth.controller.ts
@Controller('auth')
export class AuthController {
  @Get('google')
  @UseGuards(AuthGuard('google'))
  googleLogin() {}  // Redirect tới Google

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleCallback(@Req() req, @Res() res: Response) {
    const tokens = await this.authService.socialLogin(req.user);
    
    // Redirect về frontend với token
    res.redirect(
      `${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`
    );
  }
}

4. OAuth2 — GitHub ログイン

npm install passport-github2
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
  constructor(private config: ConfigService) {
    super({
      clientID: config.get('GITHUB_CLIENT_ID'),
      clientSecret: config.get('GITHUB_CLIENT_SECRET'),
      callbackURL: config.get('GITHUB_CALLBACK_URL'),
      scope: ['user:email'],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: any) {
    return {
      email: profile.emails?.[0]?.value,
      name: profile.displayName || profile.username,
      avatar: profile.photos?.[0]?.value,
      provider: 'github',
      providerId: profile.id.toString(),
    };
  }
}

5. ソーシャル ログイン サービス — アカウントのリンク

@Injectable()
export class AuthService {
  async socialLogin(socialUser: SocialUser) {
    // 1. Tìm user theo provider + providerId
    let user = await this.usersService.findByProvider(
      socialUser.provider,
      socialUser.providerId,
    );

    if (!user) {
      // 2. Tìm theo email (account linking)
      user = await this.usersService.findByEmail(socialUser.email);
      
      if (user) {
        // Link social account vào user hiện có
        await this.usersService.linkSocialAccount(user.id, {
          provider: socialUser.provider,
          providerId: socialUser.providerId,
        });
      } else {
        // 3. Tạo user mới
        user = await this.usersService.create({
          email: socialUser.email,
          name: socialUser.name,
          avatar: socialUser.avatar,
          provider: socialUser.provider,
          providerId: socialUser.providerId,
        });
      }
    }

    return this.generateTokens(user);
  }
}

6. まとめ

  • セッション: サーバー側の状態、本番環境に Redis ストアを使用
  • クッキー: HttpOnly + Secure + SameSite による安全なトークン ストレージ
  • OAuth2: Google、GitHub、Facebookによるソーシャルログイン
  • アカウントリンク: 複数のプロバイダーを 1 つのユーザー アカウントに結合します
  • Cookie 内の JWT は localStorage より安全です (XSS 対策)

次の記事で詳しく説明します インターセプター、ミドルウェア、リクエストのライフサイクル。