361 lines
9.7 KiB
TypeScript

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<TokenResponse> {
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<boolean>(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<AccessTokenResponse> {
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<DelegationTokenResponse> {
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<DelegationAccessTokenResponse> {
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 };
}
}