Merged PR 58: API実装(アカウント登録/Azure AD B2C)

## 概要
[Task1550: API実装(アカウント登録/Azure AD B2C)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1550)

- アカウント登録APIでAzure ADB2Cにユーザを登録する処理を追加しました。
  - GraphAPIで登録処理をしています。

## レビューポイント
- ADB2Cへ登録する情報は認識通りか
- ADB2CへのGraphAPI接続のためにADB2Cテナントにアプリを追加しているが認証として問題ないか。
  - 環境変数にアプリの情報を設定しています
    - ADB2C_TENANT_ID=xxxxxxxx
    - ADB2C_CLIENT_ID=xxxxxxxx
    - ADB2C_CLIENT_SECRET=xxxxxxxx

## UIの変更
無し

## 動作確認状況
- ローカルで確認
  - ADB2C、DBに設定項目が追加されていることを確認
This commit is contained in:
makabe.t 2023-04-05 09:22:50 +00:00
parent 731c633189
commit 0099614a5f
8 changed files with 164 additions and 10 deletions

View File

@ -4,6 +4,10 @@ PORT=8081
AZURE_TENANT_ID=xxxxxxxx
AZURE_CLIENT_ID=xxxxxxxx
AZURE_CLIENT_SECRET=xxxxxxxx
# 開発環境ではADB2Cが別テナントになる都合上、環境変数を分けている
ADB2C_TENANT_ID=xxxxxxxx
ADB2C_CLIENT_ID=xxxxxxxx
ADB2C_CLIENT_SECRET=xxxxxxxx
KEY_VAULT_NAME=kv-odms-secret-dev
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"

View File

@ -11,6 +11,7 @@
"dependencies": {
"@azure/identity": "^3.1.3",
"@azure/keyvault-secrets": "^4.6.0",
"@microsoft/microsoft-graph-client": "^3.0.5",
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.2.0",
@ -1031,6 +1032,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
@ -1731,6 +1743,32 @@
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"node_modules/@microsoft/microsoft-graph-client": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.5.tgz",
"integrity": "sha512-xQADFNLUhE78RzYadFZtOmy/5wBZenSZhVK193m40MTDC5hl1aYMQO1QOJApnKga8WcvMCDCU10taRhuXTOz5w==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependenciesMeta": {
"@azure/identity": {
"optional": true
},
"@azure/msal-browser": {
"optional": true
},
"buffer": {
"optional": true
},
"stream-browserify": {
"optional": true
}
}
},
"node_modules/@nestjs/axios": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.1.tgz",
@ -8987,6 +9025,11 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -11810,6 +11853,14 @@
"@babel/helper-plugin-utils": "^7.19.0"
}
},
"@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"@babel/template": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
@ -12366,6 +12417,15 @@
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"@microsoft/microsoft-graph-client": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.5.tgz",
"integrity": "sha512-xQADFNLUhE78RzYadFZtOmy/5wBZenSZhVK193m40MTDC5hl1aYMQO1QOJApnKga8WcvMCDCU10taRhuXTOz5w==",
"requires": {
"@babel/runtime": "^7.12.5",
"tslib": "^2.2.0"
}
},
"@nestjs/axios": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.1.tgz",
@ -17907,6 +17967,11 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View File

@ -27,6 +27,7 @@
"dependencies": {
"@azure/identity": "^3.1.3",
"@azure/keyvault-secrets": "^4.6.0",
"@microsoft/microsoft-graph-client": "^3.0.5",
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.2.0",

View File

@ -10,6 +10,7 @@ E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const ErrorCodes = [
'E009999', // 汎用エラー
@ -20,4 +21,5 @@ export const ErrorCodes = [
'E000105', // トークン発行元エラー
'E000106', // トークンアルゴリズムエラー
'E010201', // 未認証ユーザエラー
'E010301', // メールアドレス登録済みエラー
] as const;

View File

@ -10,4 +10,5 @@ export const errors: Errors = {
E000105: 'Token invalid issuer Error.',
E000106: 'Token invalid algorithm Error.',
E010201: 'Email not verified user Error.',
E010301: 'This email user already created Error',
};

View File

@ -40,7 +40,7 @@ export class AccountsController {
} = body;
const role = 'none';
const { accountId, userId } = await this.accountService.createAccount(
await this.accountService.createAccount(
companyName,
country,
dealerAccountId,

View File

@ -3,7 +3,11 @@ import { ConfigService } from '@nestjs/config';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import {
AdB2cService,
ConflictError,
isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { User } from '../../repositories/users/entity/user.entity';
import { TIER_5 } from '../../constants';
@ -36,7 +40,7 @@ export class AccountsService {
role: string,
acceptedTermsVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
let externalUser: { sub: string };
let externalUser: { sub: string } | ConflictError;
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
@ -47,12 +51,21 @@ export class AccountsService {
} catch (e) {
console.log(e);
console.log('create externalUser failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレス重複エラー
if (isConflictError(externalUser)) {
throw new HttpException(
makeErrorResponse('E010301'),
HttpStatus.BAD_REQUEST,
);
}
let account: Account;
let user: User;
try {

View File

@ -2,25 +2,93 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { JwkSignKey, B2cMetadata } from '../../common/token';
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { ClientSecretCredential } from '@azure/identity';
export type ConflictError = {
reason: 'email';
message: string;
};
export const isConflictError = (arg: unknown): arg is ConflictError => {
const value = arg as ConflictError;
if (value.message === undefined) {
return false;
}
if (value.reason === 'email') {
return true;
}
return false;
};
@Injectable()
export class AdB2cService {
constructor(private readonly configService: ConfigService) {}
private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName = this.configService.get<string>('TENANT_NAME');
private readonly flowName =
this.configService.get<string>('SIGNIN_FLOW_NAME');
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.get('ADB2C_TENANT_ID'),
this.configService.get('ADB2C_CLIENT_ID'),
this.configService.get('ADB2C_CLIENT_SECRET'),
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ['https://graph.microsoft.com/.default'],
});
this.graphClient = Client.initWithMiddleware({ authProvider });
}
/**
* Creates user AzureADB2Cにユーザーを追加する
* @param email
* @param password
* @param username
* @returns user
*/
async createUser(
email: string,
password: string,
username: string,
): Promise<{ sub: string }> {
// XXX GraphAPIの呼び出しを行う
console.log(email);
console.log(password);
console.log(username);
return { sub: 'xxxxxxxx' };
): Promise<{ sub: string } | ConflictError> {
this.logger.log(`[IN] ${this.createUser.name}`);
try {
// ユーザをADB2Cに登録
const newUser = await this.graphClient.api('users/').post({
accountEnabled: true,
displayName: username,
passwordProfile: {
forceChangePasswordNextSignIn: false,
password: password,
},
identities: [
{
signinType: 'emailAddress',
issuer: `${this.tenantName}.onmicrosoft.com`,
issuerAssignedId: email,
},
],
});
return { sub: newUser.id };
} catch (e) {
this.logger.error(e);
if (e?.statusCode === 400 && e?.body) {
const error = JSON.parse(e.body);
// エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す
if (error?.details?.find((x) => x.code === 'ObjectConflict')) {
return { reason: 'email', message: 'ObjectConflict' };
}
}
throw e;
} finally {
this.logger.log(`[OUT] ${this.createUser.name}`);
}
}
/**