From 0099614a5fededa00606133c10591cc6560fcbd2 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 5 Apr 2023 09:22:50 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=2058:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E7=99=BB?= =?UTF-8?q?=E9=8C=B2/Azure=20AD=20B2C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [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に設定項目が追加されていることを確認 --- dictation_server/.env.local.example | 4 + dictation_server/package-lock.json | 65 +++++++++++++++ dictation_server/package.json | 1 + dictation_server/src/common/error/code.ts | 2 + dictation_server/src/common/error/message.ts | 1 + .../features/accounts/accounts.controller.ts | 2 +- .../src/features/accounts/accounts.service.ts | 17 +++- .../src/gateways/adb2c/adb2c.service.ts | 82 +++++++++++++++++-- 8 files changed, 164 insertions(+), 10 deletions(-) diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 5140b0c..273bca3 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -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" diff --git a/dictation_server/package-lock.json b/dictation_server/package-lock.json index db830e0..41779e8 100644 --- a/dictation_server/package-lock.json +++ b/dictation_server/package-lock.json @@ -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", diff --git a/dictation_server/package.json b/dictation_server/package.json index 57c57df..de69747 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -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", diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 56c25b6..8f152c6 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -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; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index bdb49d1..66be471 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -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', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 563ef8d..38e1133 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -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, diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 50bc03c..d66ca02 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -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 { diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index 74aae9d..2e6b297 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -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('TENANT_NAME'); private readonly flowName = this.configService.get('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}`); + } } /**