diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ec7ba0e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.10.0 diff --git a/package-lock.json b/package-lock.json index b9f716f..386e350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "kysely": "^0.27.4", "lodash": "^4.17.21", "luxon": "^3.4.3", - "openid-client": "^5.6.5", + "openid-client": "6.0.0", "pg": "^8.12.0", "zulip-js": "^2.0.9" }, @@ -5635,9 +5635,10 @@ } }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -6122,6 +6123,15 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.1.tgz", + "integrity": "sha512-0h4FZjsntbKQ5IHGM9mFT7uOwQCRdcTG7YhC0xXlWIcCch24wUa6Vggaipa3Sw6Ab7nEnmO4rctROmyuHBfP7Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6130,14 +6140,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -6152,14 +6154,6 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "node_modules/oidc-token-hash": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6180,32 +6174,18 @@ } }, "node_modules/openid-client": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", - "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.0.0.tgz", + "integrity": "sha512-UAgDDO89wEI/k6o2VestofbN6MJcvNlXbVZLNDGr2gQwVQSpDZqqTtROYKc4k0fftvQgHLshDhK4w9nC78lL5g==", "license": "MIT", "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" + "jose": "^5.9.4", + "oauth4webapi": "^3.1.1" }, "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -8170,11 +8150,6 @@ "node": ">=0.4" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index 54fcf67..15a4b3c 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "format:fix": "prettier --write .", "start": "npm run start:dev", "nest": "nest", - "start:dev": "nest start --watch --", - "start:debug": "nest start --debug 9240 --watch --", + "start:dev": "nest start --watch -e 'node --experimental-require-module' --", + "start:debug": "nest start --debug 9240 --watch -e 'node --experimental-require-module' --", "lint": "eslint \"src/**/*.ts\" --max-warnings 0", "lint:fix": "npm run lint -- --fix", "check": "tsc --noEmit", @@ -46,7 +46,7 @@ "kysely": "^0.27.4", "lodash": "^4.17.21", "luxon": "^3.4.3", - "openid-client": "^5.6.5", + "openid-client": "6.0.0", "pg": "^8.12.0", "zulip-js": "^2.0.9" }, @@ -75,6 +75,6 @@ "npm": ">=7.0.0" }, "volta": { - "node": "20.18.0" + "node": "22.10.0" } } diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts index c68bc38..5863a7f 100644 --- a/src/services/oauth.service.ts +++ b/src/services/oauth.service.ts @@ -1,48 +1,37 @@ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; -import { Client, Issuer, generators } from 'openid-client'; +// @ts-expect-error f'ing ts does not let you import types from esm +import type { Configuration, ServerMetadata } from 'openid-client'; +// @ts-expect-error we have the experimental flag enabled so we can import esm packages +import client from 'openid-client'; import { getConfig } from 'src/config'; import { OAuthAuthorizeDto, OAuthCallbackDto } from 'src/dtos/oauth.dto'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; -type GithubProfile = { - login: string; - id: string; - avatar_url: string; - url: string; - type: 'User'; - name: string; - created_at: string; - updated_at: string; -}; - type StateItem = { value: string; expiresAt: number }; const stateMap = new Map(); @Injectable() export class OAuthService { private logger: Logger = new Logger(OAuthService.name); - private client: Client; + private config: Configuration; constructor(@Inject(IDatabaseRepository) private database: IDatabaseRepository) { - const issuer = new Issuer({ + const { github } = getConfig(); + const server: ServerMetadata = { issuer: 'https://github.com', authorization_endpoint: 'https://github.com/login/oauth/authorize', token_endpoint: 'https://github.com/login/oauth/access_token', userinfo_endpoint: 'https://api.github.com/user', - }); + }; - const { github } = getConfig(); - this.client = new issuer.Client({ - client_id: github.clientId, - client_secret: github.clientSecret, - }); + this.config = new client.Configuration(server, github.clientId, github.clientSecret); } authorize(dto: OAuthAuthorizeDto) { - const state = generators.state(); - stateMap.set(state, { value: state, expiresAt: Date.now() + 5 * 60 * 1000 }); + const state = client.randomState(); + stateMap.set(dto.redirectUri, { value: state, expiresAt: Date.now() + 5 * 60 * 1000 }); return { - url: this.client.authorizationUrl({ + url: client.buildAuthorizationUrl(this.config, { state, scope: 'openid profile email', redirect_uri: dto.redirectUri, @@ -53,21 +42,22 @@ export class OAuthService { async callback({ url }: OAuthCallbackDto) { try { const redirectUri = new URL(url).origin + '/claim/callback'; - const params = this.client.callbackParams(url); - if (!params.state || !stateMap.has(params.state)) { + if (!stateMap.has(redirectUri)) { throw new BadRequestException('Invalid state parameter'); } - const stateItem = stateMap.get(params.state); + const stateItem = stateMap.get(redirectUri); if (!stateItem || stateItem.expiresAt < Date.now()) { throw new BadRequestException('Invalid state parameter'); } - const tokens = await this.client.oauthCallback(redirectUri, params, { state: stateItem.value }); - const profile = await this.client.userinfo(tokens); + const tokens = await client.authorizationCodeGrant(this.config, new URL(redirectUri), { + expectedState: stateItem.value, + }); + const profile = await client.fetchUserInfo(this.config, tokens.access_token, tokens.claims()?.sub || ''); - const licenses = await this.database.getSponsorLicenses(profile.login); + const licenses = await this.database.getSponsorLicenses(profile.sub); return { username: profile.login,