Merged PR 1: タスク 1359: API実装(認証/IDトークン検証)

## 概要
[タスク 1359: API実装(認証/IDトークン検証)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/1359)

- IDトークンを受け取って内容を検証し、デコードしたペイロードを返すサービスを実装しました。

## レビューポイント
- サービスの処理の流れが認識とあっているか。
- ADB2CのAPI呼び出しを別サービスにしているが問題ないか
- 公開鍵の変換処理を別サービスに切り出しているが構成に問題はないか。
- トークンの検証をエラーごとに処理しているがエラー内容は認識通りか

## UIの変更
- なし

## 動作確認状況
- テストが通ることを確認

# 備考
- IDトークンを検証して中身を返すまでの実装です。
This commit is contained in:
makabe.t 2023-03-07 01:30:13 +00:00
parent bf7a985b0d
commit 41f0213fe9
19 changed files with 915 additions and 59 deletions

View File

@ -5,4 +5,6 @@ DB_NAME=omds
DB_ROOT_PASS=omdsdbpass
DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass
NO_COLOR=TRUE
NO_COLOR=TRUE
TENANT_NAME=adb2codmsdev
SIGNIN_FLOW_NAME=b2c_1_signin_dev

View File

@ -9,7 +9,6 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^8.0.0",
@ -17,12 +16,16 @@
"@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^9.0.1",
"@openapitools/openapi-generator-cli": "^2.5.1",
"@types/jsonwebtoken": "^9.0.1",
"@types/uuid": "^8.3.4",
"axios": "^1.3.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"connect-redis": "^6.1.3",
"cookie-parser": "^1.4.6",
"express-session": "^1.17.3",
"jsonwebtoken": "^9.0.0",
"jwk-to-pem": "^2.0.5",
"mysql2": "^2.3.3",
"redis": "^4.2.0",
"reflect-metadata": "^0.1.13",
@ -40,10 +43,13 @@
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/jest": "27.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/jwk-to-pem": "^2.0.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"base64url": "^3.0.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
@ -1460,19 +1466,6 @@
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"node_modules/@nestjs/axios": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.1.tgz",
"integrity": "sha512-rLEq6yfho2CZyOcxP+P4Q3FjkNuiiHDyzj3Cr9i4Kdn3Ng09ygtOB4++jjXPREc6650pOFfzNtw18QH7bfLnQA==",
"dependencies": {
"axios": "1.2.1"
},
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
"reflect-metadata": "^0.1.12",
"rxjs": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@nestjs/cli": {
"version": "8.2.8",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.8.tgz",
@ -2561,6 +2554,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
"integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/jwk-to-pem": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.1.tgz",
"integrity": "sha512-QXmRPhR/LPzvXBHTPfG2BBfMTkNLUD7NyRcPft8m5xFCeANa1BZyLgT0Gw+OxdWx6i1WCpT27EqyggP4UUHMrA==",
"dev": true
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@ -3243,15 +3251,26 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz",
"integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@ -3373,6 +3392,15 @@
}
]
},
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -3428,6 +3456,11 @@
"node": ">= 6"
}
},
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/body-parser": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
@ -3485,6 +3518,11 @@
"node": ">=8"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
},
"node_modules/browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
@ -3534,6 +3572,11 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -4367,6 +4410,14 @@
"wcwidth": ">=1.0.1"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4378,6 +4429,20 @@
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"node_modules/elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/emittery": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
@ -5563,6 +5628,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@ -5580,6 +5654,16 @@
"node": "*"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -7038,6 +7122,50 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
"integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
"dependencies": {
"jws": "^3.2.2",
"lodash": "^4.17.21",
"ms": "^2.1.1",
"semver": "^7.3.8"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwk-to-pem": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
"dependencies": {
"asn1.js": "^5.3.0",
"elliptic": "^6.5.4",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -7419,6 +7547,16 @@
"node": ">=6"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -8624,7 +8762,6 @@
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -8639,7 +8776,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -8650,8 +8786,7 @@
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/send": {
"version": "0.18.0",
@ -11560,14 +11695,6 @@
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"@nestjs/axios": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.1.tgz",
"integrity": "sha512-rLEq6yfho2CZyOcxP+P4Q3FjkNuiiHDyzj3Cr9i4Kdn3Ng09ygtOB4++jjXPREc6650pOFfzNtw18QH7bfLnQA==",
"requires": {
"axios": "1.2.1"
}
},
"@nestjs/cli": {
"version": "8.2.8",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.2.8.tgz",
@ -12389,6 +12516,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/jsonwebtoken": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
"integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/jwk-to-pem": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.1.tgz",
"integrity": "sha512-QXmRPhR/LPzvXBHTPfG2BBfMTkNLUD7NyRcPft8m5xFCeANa1BZyLgT0Gw+OxdWx6i1WCpT27EqyggP4UUHMrA==",
"dev": true
},
"@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@ -12913,15 +13055,26 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz",
"integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@ -13008,6 +13161,12 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"dev": true
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -13045,6 +13204,11 @@
}
}
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"body-parser": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
@ -13097,6 +13261,11 @@
"fill-range": "^7.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
},
"browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
@ -13127,6 +13296,11 @@
"node-int64": "^0.4.0"
}
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -13734,6 +13908,14 @@
"wcwidth": ">=1.0.1"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -13745,6 +13927,20 @@
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"emittery": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
@ -14638,6 +14834,15 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@ -14649,6 +14854,16 @@
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -15770,6 +15985,46 @@
"universalify": "^2.0.0"
}
},
"jsonwebtoken": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
"integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
"requires": {
"jws": "^3.2.2",
"lodash": "^4.17.21",
"ms": "^2.1.1",
"semver": "^7.3.8"
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jwk-to-pem": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
"requires": {
"asn1.js": "^5.3.0",
"elliptic": "^6.5.4",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -16065,6 +16320,16 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -16968,7 +17233,6 @@
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
},
@ -16977,7 +17241,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
@ -16985,8 +17248,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},

View File

@ -25,7 +25,6 @@
"swgbundle:sentinel": "swagger-cli bundle -o openapi/build/bundle.yml -t yaml openapi/sentinel_ems/sentinel_ems.yml"
},
"dependencies": {
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^8.0.0",
@ -33,12 +32,16 @@
"@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^9.0.1",
"@openapitools/openapi-generator-cli": "^2.5.1",
"@types/jsonwebtoken": "^9.0.1",
"@types/uuid": "^8.3.4",
"axios": "^1.3.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"connect-redis": "^6.1.3",
"cookie-parser": "^1.4.6",
"express-session": "^1.17.3",
"jsonwebtoken": "^9.0.0",
"jwk-to-pem": "^2.0.5",
"mysql2": "^2.3.3",
"redis": "^4.2.0",
"reflect-metadata": "^0.1.13",
@ -56,10 +59,13 @@
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/jest": "27.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/jwk-to-pem": "^2.0.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"base64url": "^3.0.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",

View File

@ -6,6 +6,9 @@ import { join } from 'path';
import { LoggerMiddleware } from './common/loggerMiddleware';
import { AuthModule } from './features/auth/auth.module';
import { AuthController } from './features/auth/auth.controller';
import { AuthService } from './features/auth/auth.service';
import { CryptoModule } from './gateways/crypto/crypto.module';
import { AdB2cModule } from './gateways/adb2c/adb2c.module';
@Module({
imports: [
@ -17,6 +20,8 @@ import { AuthController } from './features/auth/auth.controller';
isGlobal: true,
}),
AuthModule,
CryptoModule,
AdB2cModule,
// TypeOrmModule.forRootAsync({
// imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => ({
@ -33,7 +38,7 @@ import { AuthController } from './features/auth/auth.controller';
// }),
],
controllers: [HealthController, AuthController],
providers: [],
providers: [AuthService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {

View File

@ -1,4 +1,22 @@
//TODO 仮のエラーコード作成
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
*/
export const ErrorCodes = [
'E009999', // 汎用エラー
'E000101', // トークン形式不正エラー
'E000102', // トークン有効期限切れエラー
'E000103', // トークン非アクティブエラー
'E000104', // トークン署名エラー
'E000105', // トークン発行元エラー
'E000106', // トークンアルゴリズムエラー
] as const;

View File

@ -2,5 +2,11 @@ import { Errors } from './types/types';
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: 'Internal Server Error',
E009999: 'Internal Server Error.',
E000101: 'Token invalid format Error.',
E000102: 'Token expired Error.',
E000103: 'Token not before Error',
E000104: 'Token invalid signature Error.',
E000105: 'Token invalid issuer Error.',
E000106: 'Token invalid algorithm Error.',
};

View File

@ -0,0 +1,20 @@
export type RefreshToken = {
scope: string;
};
export type AccessToken = {
scope: string;
};
export type B2cMetadata = {
issuer: string;
};
export type JwkSignKey = {
kid: string;
nbf: number;
use: string;
kty: string;
e: string;
n: string;
};

View File

@ -1,5 +1,10 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import {
makeAdB2cServiceMock,
makeDefaultAdB2cMockValue,
} from './test/auth.service.mock';
describe('AuthController', () => {
let controller: AuthController;
@ -7,7 +12,10 @@ describe('AuthController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
providers: [AuthService],
})
.useMocker(() => makeAdB2cServiceMock(makeDefaultAdB2cMockValue()))
.compile();
controller = module.get<AuthController>(AuthController);
});

View File

@ -6,14 +6,18 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { ErrorResponse } from '../../common/error/types/types';
import { AuthService } from './auth.service';
import {
AccessTokenResponse,
TokenRequest,
TokenResponse,
} from './types/types';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('token')
@ApiResponse({
status: HttpStatus.OK,
@ -32,7 +36,8 @@ export class AuthController {
})
@ApiOperation({ operationId: 'token' })
async token(@Body() body: TokenRequest): Promise<TokenResponse> {
console.log(body);
const payload = await this.authService.getVerifiedIdToken(body.idToken);
console.log(payload);
return {
accessToken: '',
@ -61,6 +66,14 @@ export class AuthController {
async accessToken(@Headers() headers): Promise<AccessTokenResponse> {
console.log(headers['authorization']);
const authHeader = headers['authorization'];
let refreshToken = '';
if (typeof authHeader === 'string') {
refreshToken = authHeader.replace('Bearer ', '');
}
console.log(refreshToken);
return { accessToken: '' };
}
}

View File

@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { AdB2cModule } from 'src/gateways/adb2c/adb2c.module';
import { CryptoModule } from '../../gateways/crypto/crypto.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [CryptoModule, AdB2cModule],
controllers: [AuthController],
providers: [AuthService],
})

View File

@ -1,18 +1,134 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
makeAdB2cServiceMock,
makeAuthServiceMock,
makeDefaultAdB2cMockValue,
makeDefaultCryptoMockValue,
} from './test/auth.service.mock';
describe('AuthService', () => {
let service: AuthService;
it('IDトークンの検証とペイロードの取得に成功する', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdXNlciJ9.dURTqFfUc94qYNFS65xMqMT3tJi-uH8weJ5GU74F-UvhLKJmqVoNIbXCm-ASg2mZ5SxeX683q2MMrlzxNcNel-QIFTj0zWem4eLKuarsYRMTU8x27jMss0b_JkFtiy5KE2052_fLeW2aZBb4AjieIW34-c0Ol1rrih-W9Pk6ZEOwf14Ju0IAgdUw66-HuLOUprZjavvFp2rwuAC7gqEujg3wtjO7VR82NNoVv2dxByTdrzG_eOk8QKTJYBoM50ztT6WFs-qWetZIoQDxMJRA9ObfEVyJ4Bq2b4qelfuhAqbQWRNfCvqhoRPMJI6Y7-R94FuO1zg4d4Gm3J0qHj7r8Q';
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
expect(await service.getVerifiedIdToken(token)).toEqual(idTokenPayload);
});
it('should be defined', () => {
expect(service).toBeDefined();
it('IDトークンの形式が不正な場合、形式不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token = 'invalid.id.token';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000101'), HttpStatus.UNAUTHORIZED),
);
});
it('IDトークンの有効期限が切れている場合、有効期限切れエラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjEwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdWVyIn0.pk9-POXmo3ie7wfqizRr2NSX7YGEJed3krdMImzKp5qeRfsEJwM6zTSzjL_gbLQyMEDg3IoyWzVVUha4sdPcummtt8gJ6N25s_H9taFY-xJjx_-wmttGQvXD44apyUF_c_Xg0l7MzNQEqpnzsLdDcBITSN-Rk-bT_n9U4dxhYaWukOPfAPf7DzSxT9TCpTaCTPIAKp2JXVU41J9uv5WklhNjMSv9L5jWT-IstEC71-DO6jD6yFpAOLIG6Aq-C7rQolt_Cny9qKugu6hd7_Aj-YrW9773khUOyFotJ7Qs9g-qSEMy7M9ljvai2eu5Otfja3hlS5wF1VH_ENsXye6C0A';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000102'), HttpStatus.UNAUTHORIZED),
);
});
it('IDトークンが開始日より前の場合、開始前エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6OTAwMDAwMDAwMCwiaXNzIjoiaXNzdWVyIn0.pSk3U8Wn_XTm6KxvdQpqQuGROjanLxLGsnRGg35jO4iPVysHKVWya7Leik3aTtK8lGCSoapodQoV4EXbP5CzBf5O83l9tKaF2pwEO_C9QePlLJIJMaCmqRFrr1ozcVFQNwYAr81KNmXBuEEG5duT36Fk2A9-PLDtwg816J4nMEnd1umgCSRTKOdxh265ybDxX1Pe8KDtzlCi8Wjj-lhFwxskpKD3xlFl2plhW9_P7eq-DJXtm1fvLk27zxwOq8uaaxu6sfXoeu1D4qi4aZ0MF-5eaOezc8KH-aYgU9o5yQCZH6tJJDrZvzi7GAFTd-c952V9jgPZpifEoDMRJXS5zA';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000103'), HttpStatus.UNAUTHORIZED),
);
});
it('IDトークンの署名が不正な場合、署名不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdWVyIn0.sign';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000104'), HttpStatus.UNAUTHORIZED),
);
});
it('IDトークンの発行元が想定と異なる場合、発行元不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaW52YWxpZCJ9.jmwBBc2r55YOr88W_4aRzFccmtmQfQRLob7YGM4bOPMGK-ExXJQG24CiMpODZEvVTtQKucEd2cBB6zzLYGCiKRHig8iM_EV57klMwKrczdL2ov8Hc3IyA407idDxDi0KyyveEctxEUdl3PgbY54CVc6URhUAzGdcQ2mKMRVe-Zxb7CrFYqwiHUCmCnSRFUYj_9kkI3epkdsXPsLyDApDEbLEeD1ztnoyYrbfmdce6flpf-h1r2VK4dMz8m4GMOLQHM9gWSzRUkUsN3hGOKTofUxCKC4CArfSP1vZ5k9FZrjqOZn1m6bxKalCox2n96GqLtuFV3-sOTnCqgHj0fQEJA';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000105'), HttpStatus.UNAUTHORIZED),
);
});
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。メタデータ', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
adb2cParam.getMetaData = new Error('failed get metadata');
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdXNlciJ9.dURTqFfUc94qYNFS65xMqMT3tJi-uH8weJ5GU74F-UvhLKJmqVoNIbXCm-ASg2mZ5SxeX683q2MMrlzxNcNel-QIFTj0zWem4eLKuarsYRMTU8x27jMss0b_JkFtiy5KE2052_fLeW2aZBb4AjieIW34-c0Ol1rrih-W9Pk6ZEOwf14Ju0IAgdUw66-HuLOUprZjavvFp2rwuAC7gqEujg3wtjO7VR82NNoVv2dxByTdrzG_eOk8QKTJYBoM50ztT6WFs-qWetZIoQDxMJRA9ObfEVyJ4Bq2b4qelfuhAqbQWRNfCvqhoRPMJI6Y7-R94FuO1zg4d4Gm3J0qHj7r8Q';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。キーセット', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
adb2cParam.getSignKeySets = new Error('failed get keyset');
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdXNlciJ9.dURTqFfUc94qYNFS65xMqMT3tJi-uH8weJ5GU74F-UvhLKJmqVoNIbXCm-ASg2mZ5SxeX683q2MMrlzxNcNel-QIFTj0zWem4eLKuarsYRMTU8x27jMss0b_JkFtiy5KE2052_fLeW2aZBb4AjieIW34-c0Ol1rrih-W9Pk6ZEOwf14Ju0IAgdUw66-HuLOUprZjavvFp2rwuAC7gqEujg3wtjO7VR82NNoVv2dxByTdrzG_eOk8QKTJYBoM50ztT6WFs-qWetZIoQDxMJRA9ObfEVyJ4Bq2b4qelfuhAqbQWRNfCvqhoRPMJI6Y7-R94FuO1zg4d4Gm3J0qHj7r8Q';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('Azure ADB2Cから取得した鍵が一致しない場合、エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
adb2cParam.getSignKeySets = [
{ kid: 'invalid', kty: 'RSA', nbf: 0, use: 'sig', e: '', n: '' },
];
const cryptoParam = makeDefaultCryptoMockValue();
const service = await makeAuthServiceMock(adb2cParam, cryptoParam);
const token =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwiaXNzIjoiaXNzdXNlciJ9.dURTqFfUc94qYNFS65xMqMT3tJi-uH8weJ5GU74F-UvhLKJmqVoNIbXCm-ASg2mZ5SxeX683q2MMrlzxNcNel-QIFTj0zWem4eLKuarsYRMTU8x27jMss0b_JkFtiy5KE2052_fLeW2aZBb4AjieIW34-c0Ol1rrih-W9Pk6ZEOwf14Ju0IAgdUw66-HuLOUprZjavvFp2rwuAC7gqEujg3wtjO7VR82NNoVv2dxByTdrzG_eOk8QKTJYBoM50ztT6WFs-qWetZIoQDxMJRA9ObfEVyJ4Bq2b4qelfuhAqbQWRNfCvqhoRPMJI6Y7-R94FuO1zg4d4Gm3J0qHj7r8Q';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});
const idTokenPayload = {
exp: 9000000000,
nbf: 1000000000,
iss: 'issuser',
};

View File

@ -1,4 +1,123 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import jwt from 'jsonwebtoken';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { CryptoService } from '../../gateways/crypto/crypto.service';
@Injectable()
export class AuthService {}
export class AuthService {
constructor(
private adB2cService: AdB2cService,
private cryptoService: CryptoService,
) {}
private readonly logger = new Logger(AuthService.name);
async getVerifiedIdToken(token: string): Promise<any> {
this.logger.log(`[IN] ${this.getVerifiedIdToken.name}`);
let kid = '';
try {
// JWTトークンのヘッダを見るため一度デコードする
const decodedToken = jwt.decode(token, { complete: true });
kid = decodedToken.header.kid;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
let issuer = '';
try {
const metadata = await this.adB2cService.getMetaData();
const keySets = await this.adB2cService.getSignKeySets();
issuer = metadata.issuer;
const jwkKey = keySets.find((x) => x.kid === kid);
if (!jwkKey) {
throw new Error('Public Key Not Found.');
}
const publicKey = await this.cryptoService.getPublicKeyFromJwk(jwkKey);
const verifiedToken = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: [issuer],
});
return verifiedToken;
} catch (e) {
if (e instanceof Error) {
const { name, message } = e;
this.logger.error(`error=${name}: ${message}`);
switch (e.constructor) {
case jwt.TokenExpiredError:
throw new HttpException(
makeErrorResponse('E000102'),
HttpStatus.UNAUTHORIZED,
);
case jwt.NotBeforeError:
throw new HttpException(
makeErrorResponse('E000103'),
HttpStatus.UNAUTHORIZED,
);
case jwt.JsonWebTokenError:
// メッセージごとにエラーを判定しHTTPエラーを生成
throw this.makeHttpErrorFromJsonWebTokenErrorMessage(
message,
issuer,
);
default:
break;
}
} else {
this.logger.error(`error=${e}`);
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${this.getVerifiedIdToken.name}`);
}
}
/**
* JWT検証時のErrorJsonWebTokenErrorをメッセージごとに仕分けてHTTPエラーを生成
*/
makeHttpErrorFromJsonWebTokenErrorMessage = (
message: string,
issuer: string,
): Error => {
// 署名が不正
if (message === 'invalid signature') {
return new HttpException(
makeErrorResponse('E000104'),
HttpStatus.UNAUTHORIZED,
);
}
// 想定発行元と異なる
if (message === `jwt issuer invalid. expected: ${issuer}`) {
return new HttpException(
makeErrorResponse('E000105'),
HttpStatus.UNAUTHORIZED,
);
}
// アルゴリズムが想定と異なる
if (message === 'invalid algorithm') {
return new HttpException(
makeErrorResponse('E000106'),
HttpStatus.UNAUTHORIZED,
);
}
// トークンの形式が不正
return new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
};
}

View File

@ -0,0 +1,100 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdB2cService } from '../../../gateways/adb2c/adb2c.service';
import { CryptoService } from '../../../gateways/crypto/crypto.service';
import { JwkSignKey, B2cMetadata } from '../../../common/token';
import { AuthService } from '../auth.service';
export type AdB2cMockValue = {
getMetaData: B2cMetadata | Error;
getSignKeySets: JwkSignKey[] | Error;
};
export type CryptoMockValue = {
getPublicKeyFromJwk: string | Error;
};
export const makeAuthServiceMock = async (
adB2cMockValue: AdB2cMockValue,
cryptoMockValue: CryptoMockValue,
): Promise<AuthService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
})
.useMocker((token) => {
switch (token) {
case AdB2cService:
return makeAdB2cServiceMock(adB2cMockValue);
case CryptoService:
return makeCryptoServiceMock(cryptoMockValue);
}
})
.compile();
return module.get<AuthService>(AuthService);
};
export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
return {
getMetaData: {
issuer: 'issuser',
},
getSignKeySets: [
{
kid: 'kid',
nbf: 1111111111,
use: 'sig',
kty: 'RSA',
e: 'e',
n: 'n',
},
],
};
};
export const makeAdB2cServiceMock = (value: AdB2cMockValue) => {
const { getMetaData, getSignKeySets } = value;
return {
getMetaData:
getMetaData instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(getMetaData)
: jest.fn<Promise<B2cMetadata>, []>().mockResolvedValue(getMetaData),
getSignKeySets:
getSignKeySets instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(getSignKeySets)
: jest
.fn<Promise<JwkSignKey[]>, []>()
.mockResolvedValue(getSignKeySets),
};
};
export const makeDefaultCryptoMockValue = (): CryptoMockValue => {
return {
getPublicKeyFromJwk: [
'-----BEGIN PUBLIC KEY-----',
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD',
'jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23',
'tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5',
'7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD',
'alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2',
'1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a',
'IQIDAQAB',
'-----END PUBLIC KEY-----',
].join('\n'),
};
};
export const makeCryptoServiceMock = (value: CryptoMockValue) => {
const { getPublicKeyFromJwk } = value;
return {
getPublicKeyFromJwk:
getPublicKeyFromJwk instanceof Error
? jest
.fn<Promise<void>, [JwkSignKey]>()
.mockRejectedValue(getPublicKeyFromJwk)
: jest
.fn<Promise<string>, [JwkSignKey]>()
.mockResolvedValue(getPublicKeyFromJwk),
};
};

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AdB2cService } from './adb2c.service';
@Module({
imports: [ConfigModule],
exports: [AdB2cService],
providers: [AdB2cService],
})
export class AdB2cModule {}

View File

@ -0,0 +1,60 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { JwkSignKey, B2cMetadata } from '../../common/token';
@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');
/**
* ADB2Cのメタデータを取得する
* @returns meta data
*/
async getMetaData(): Promise<B2cMetadata> {
this.logger.log(`[IN] ${this.getMetaData.name}`);
try {
// Azure AD B2Cのメタデータを取得する。 以下のURLから取得できる。
// https://<テナント名>.b2clogin.com/<テナント名>.onmicrosoft.com/<ユーザーフロー名>/v2.0/.well-known/openid-configuration
const metaData = await axios
.get(
`https://${this.tenantName}.b2clogin.com/${this.tenantName}.onmicrosoft.com/${this.flowName}/v2.0/.well-known/openid-configuration`,
)
.then((res) => res.data);
return metaData;
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.getMetaData.name}`);
}
}
/**
* IDトークンの署名キーセットを取得する
* @returns sign key sets
*/
async getSignKeySets(): Promise<JwkSignKey[]> {
this.logger.log(`[IN] ${this.getSignKeySets.name}`);
try {
// 署名キーのキーセット配列を取得する。 以下のURLから取得できる。
const keySets = await axios
.get(
`https://${this.tenantName}.b2clogin.com/${this.tenantName}.onmicrosoft.com/${this.flowName}/discovery/v2.0/keys`,
)
.then((res) => res.data.keys);
return keySets;
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.getSignKeySets.name}`);
}
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CryptoService } from './crypto.service';
@Module({
imports: [ConfigModule],
exports: [CryptoService],
providers: [CryptoService],
})
export class CryptoModule {}

View File

@ -0,0 +1,92 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import jwkToPem from 'jwk-to-pem';
import { JwkSignKey } from 'src/common/token';
@Injectable()
export class CryptoService {
constructor(private readonly configService: ConfigService) {}
private readonly logger = new Logger(CryptoService.name);
async getPrivateKey(): Promise<string> {
try {
// XXX 動作確認用
const privateKey = [
'-----BEGIN RSA PRIVATE KEY-----',
'MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F',
'GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a',
'fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa',
'FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq',
'zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF',
'+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN',
'LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq',
'L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0',
'WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE',
'x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm',
'+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX',
'cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc',
'BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H',
'IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+',
'y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb',
'iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9',
'BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95',
'mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/',
'O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s',
'xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust',
'nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT',
'tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK',
'YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM',
'iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk',
'JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==',
'-----END RSA PRIVATE KEY-----',
].join('\n');
return privateKey;
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.getPublicKey.name}`);
}
}
async getPublicKey(): Promise<string> {
try {
// XXX 動作確認用
const publicKey = [
'-----BEGIN PUBLIC KEY-----',
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD',
'jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23',
'tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5',
'7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD',
'alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2',
'1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a',
'IQIDAQAB',
'-----END PUBLIC KEY-----',
].join('\n');
return publicKey;
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.getPublicKey.name}`);
}
}
async getPublicKeyFromJwk(jwkKey: JwkSignKey): Promise<string> {
try {
// JWK形式のJSONなのでJWTの公開鍵として使えるようにPEM形式に変換
const publicKey = jwkToPem({
kty: 'RSA',
n: jwkKey.n,
e: jwkKey.e,
});
return publicKey;
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.getPublicKey.name}`);
}
}
}

View File

@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
import cookieParser from 'cookie-parser';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
@ -12,7 +12,12 @@ async function bootstrap() {
app.use(new LoggerMiddleware(), cookieParser());
// バリデーター(+型の自動変換機能)を適用
app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.useGlobalPipes(
new ValidationPipe({
transform: true,
forbidUnknownValues: false,
}),
);
if (process.env.STAGE === 'local') {
const options = new DocumentBuilder()

View File

@ -16,6 +16,7 @@
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true
}
}