361 lines
9.7 KiB
TypeScript
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 };
|
|
}
|
|
}
|