import { Body, Controller, HttpException, HttpStatus, Logger, Post, Req, UseGuards, } from '@nestjs/common'; import { ApiResponse, ApiOperation, ApiBearerAuth, ApiTags, } from '@nestjs/swagger'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { ErrorResponse } from '../../common/error/types/types'; import { AuthService } from './auth.service'; import { AccessTokenResponse, TokenRequest, TokenResponse, DelegationTokenRequest, DelegationTokenResponse, DelegationAccessTokenResponse, } from './types/types'; import { retrieveAuthorizationToken } from '../../common/http/helper'; import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log'; import { Request } from 'express'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES, TIERS } from '../../constants'; import jwt from 'jsonwebtoken'; import { AccessToken, RefreshToken } from '../../common/token'; import { makeIDTokenKey } from '../../common/cache'; import { RedisService } from '../../gateways/redis/redis.service'; @ApiTags('auth') @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); constructor( private readonly authService: AuthService, private readonly redisService: RedisService, ) {} @Post('token') @ApiResponse({ status: HttpStatus.OK, type: TokenResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー/同意済み利用規約が最新でない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ description: 'AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します', operationId: 'token', }) async token( @Body() body: TokenRequest, @Req() req: Request, ): Promise { const ip = retrieveIp(req); if (!ip) { throw new HttpException( makeErrorResponse('E000401'), HttpStatus.UNAUTHORIZED, ); } const requestId = retrieveRequestId(req); if (!requestId) { throw new HttpException( makeErrorResponse('E000501'), HttpStatus.INTERNAL_SERVER_ERROR, ); } const context = makeContext('anonymous', requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); const idToken = await this.authService.getVerifiedIdToken( context, body.idToken, ); const isVerified = await this.authService.isVerifiedUser(context, idToken); if (!isVerified) { throw new HttpException( makeErrorResponse('E010201'), HttpStatus.BAD_REQUEST, ); } const key = makeIDTokenKey(body.idToken); const isTokenExists = await this.redisService.get(context, key); if (isTokenExists) { // IDトークンがキャッシュに存在する場合エラー throw new HttpException( makeErrorResponse('E000106'), HttpStatus.UNAUTHORIZED, ); } // 同意済み利用規約バージョンが最新かチェック const isAcceptedLatestVersion = await this.authService.isAcceptedLatestVersion(context, idToken); // 最新でなければエラー if (!isAcceptedLatestVersion) { throw new HttpException( makeErrorResponse('E010209'), HttpStatus.UNAUTHORIZED, ); } // IDトークンをキャッシュに保存(idTokenの有効期限をADB2Cの有効期限と合わせる(300秒)) await this.redisService.set(context, key, true, 300); const refreshToken = await this.authService.generateRefreshToken( context, idToken, body.type, ); const accessToken = await this.authService.generateAccessToken( context, refreshToken, ); return { accessToken, refreshToken, }; } @Post('accessToken') @ApiBearerAuth() @ApiResponse({ status: HttpStatus.OK, type: AccessTokenResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'accessToken', description: 'リフレッシュトークンを元にアクセストークンを再生成します', }) async accessToken(@Req() req: Request): Promise { const refreshToken = retrieveAuthorizationToken(req); if (!refreshToken) { throw new HttpException( makeErrorResponse('E000107'), HttpStatus.UNAUTHORIZED, ); } const ip = retrieveIp(req); if (!ip) { throw new HttpException( makeErrorResponse('E000401'), HttpStatus.UNAUTHORIZED, ); } const requestId = retrieveRequestId(req); if (!requestId) { throw new HttpException( makeErrorResponse('E000501'), HttpStatus.INTERNAL_SERVER_ERROR, ); } const context = makeContext('anonymous', requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); const accessToken = await this.authService.generateAccessToken( context, refreshToken, ); return { accessToken }; } @Post('delegation/token') @ApiBearerAuth() @ApiResponse({ status: HttpStatus.OK, type: DelegationTokenResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '指定したアカウントが代行操作を許可していない場合', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'delegationToken', description: '代行操作用のリフレッシュトークン・アクセストークンを生成します', }) @UseGuards(AuthGuard) @UseGuards( RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER4], }), ) async delegationToken( @Req() req: Request, @Body() body: DelegationTokenRequest, ): Promise { const { delegatedAccountId } = body; const token = retrieveAuthorizationToken(req); if (!token) { throw new HttpException( makeErrorResponse('E000107'), HttpStatus.UNAUTHORIZED, ); } const ip = retrieveIp(req); if (!ip) { throw new HttpException( makeErrorResponse('E000401'), HttpStatus.UNAUTHORIZED, ); } const requestId = retrieveRequestId(req); if (!requestId) { throw new HttpException( makeErrorResponse('E000501'), HttpStatus.INTERNAL_SERVER_ERROR, ); } const decodedAccessToken = jwt.decode(token, { json: true }); if (!decodedAccessToken) { throw new HttpException( makeErrorResponse('E000101'), HttpStatus.UNAUTHORIZED, ); } const { userId } = decodedAccessToken as AccessToken; const context = makeContext('anonymous', requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); const refreshToken = await this.authService.generateDelegationRefreshToken( context, userId, delegatedAccountId, ); const accessToken = await this.authService.generateDelegationAccessToken( context, refreshToken, ); return { accessToken, refreshToken }; } @Post('delegation/access-token') @ApiBearerAuth() @ApiResponse({ status: HttpStatus.OK, type: DelegationAccessTokenResponse, description: '成功時のレスポンス', }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: '認証エラー', type: ErrorResponse, }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', type: ErrorResponse, }) @ApiOperation({ operationId: 'delegationAccessToken', description: '代行操作用のアクセストークンを再生成します', }) async delegationAccessToken( @Req() req: Request, ): Promise { const refreshToken = retrieveAuthorizationToken(req); if (!refreshToken) { throw new HttpException( makeErrorResponse('E000107'), HttpStatus.UNAUTHORIZED, ); } const ip = retrieveIp(req); if (!ip) { throw new HttpException( makeErrorResponse('E000401'), HttpStatus.UNAUTHORIZED, ); } const requestId = retrieveRequestId(req); if (!requestId) { throw new HttpException( makeErrorResponse('E000501'), HttpStatus.INTERNAL_SERVER_ERROR, ); } const decodedRefreshToken = jwt.decode(refreshToken, { json: true }); if (!decodedRefreshToken) { throw new HttpException( makeErrorResponse('E000101'), HttpStatus.UNAUTHORIZED, ); } const { userId, delegateUserId } = decodedRefreshToken as RefreshToken; const context = makeContext('anonymous', requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); const accessToken = await this.authService.updateDelegationAccessToken( context, delegateUserId, userId, refreshToken, ); return { accessToken }; } }