diff --git a/package.json b/package.json index 36aa60ad3..f99dfd957 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "jws": "^4.0.0", "migrate": "^2.0.1", "nordigen-node": "^1.4.0", - "openid-client": "^5.4.2", + "openid-client": "^6.1.7", "uuid": "^9.0.0", "winston": "^3.14.2" }, diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 2080a3bcb..17cba7979 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,12 +1,20 @@ import getAccountDb, { clearExpiredSessions } from '../account-db.js'; import * as uuid from 'uuid'; -import { generators, Issuer } from 'openid-client'; +import * as openIdClient from 'openid-client'; import finalConfig from '../load-config.js'; import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; import { getUserByUsername, transferAllFilesFromUser, } from '../services/user-service.js'; +import { webcrypto } from 'node:crypto'; +import fetch from 'node-fetch'; + +/* eslint-disable-next-line no-undef */ +if (!globalThis.crypto) { + /* eslint-disable-next-line no-undef */ + globalThis.crypto = webcrypto; +} export async function bootstrapOpenId(config) { if (!('issuer' in config)) { @@ -48,25 +56,87 @@ export async function bootstrapOpenId(config) { } async function setupOpenIdClient(config) { - let issuer = - typeof config.issuer === 'string' - ? await Issuer.discover(config.issuer) - : new Issuer({ - issuer: config.issuer.name, - authorization_endpoint: config.issuer.authorization_endpoint, - token_endpoint: config.issuer.token_endpoint, - userinfo_endpoint: config.issuer.userinfo_endpoint, - }); + let discovered; + if (typeof config.issuer === 'string') { + discovered = await openIdClient.discovery( + new URL(config.issuer), + config.client_id, + config.client_secret, + ); + } else { + const serverMetadata = { + issuer: config.issuer.name, + authorization_endpoint: config.issuer.authorization_endpoint, + token_endpoint: config.issuer.token_endpoint, + userinfo_endpoint: config.issuer.userinfo_endpoint, + }; + discovered = new openIdClient.Configuration( + serverMetadata, + config.client_id, + config.client_secret, + ); + } - const client = new issuer.Client({ - client_id: config.client_id, - client_secret: config.client_secret, - redirect_uri: new URL( - '/openid/callback', - config.server_hostname, - ).toString(), - validate_id_token: true, - }); + const client = { + redirect_uris: [ + new URL('/openid/callback', config.server_hostname).toString(), + ], + authorizationUrl(params) { + const urlObj = openIdClient.buildAuthorizationUrl(discovered, { + ...params, + redirect_uri: this.redirect_uris[0], + }); + return urlObj.href; + }, + async callback(redirectUri, params, checks) { + const currentUrl = new URL(redirectUri); + currentUrl.searchParams.set('code', params.code); + const tokens = await openIdClient.authorizationCodeGrant( + discovered, + currentUrl, + { + pkceCodeVerifier: checks.code_verifier, + idTokenExpected: true, + }, + ); + return { + access_token: tokens.access_token, + expires_at: tokens.expires_at, + claims: () => tokens.claims?.(), + }; + }, + async grant(grantParams) { + const currentUrl = new URL(this.redirect_uris[0]); + currentUrl.searchParams.set('code', grantParams.code); + currentUrl.searchParams.set('state', grantParams.state); + const tokens = await openIdClient.authorizationCodeGrant( + discovered, + currentUrl, + { + pkceCodeVerifier: grantParams.code_verifier, + expectedState: grantParams.state, + idTokenExpected: false, + }, + ); + return { + access_token: tokens.access_token, + expires_at: tokens.expires_at, + claims: () => tokens.claims?.(), + }; + }, + async userinfo(accessToken, sub = '') { + if (!config.authMethod || config.authMethod === 'openid') { + return openIdClient.fetchUserInfo(discovered, accessToken, sub); + } else { + const response = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.json(); + } + }, + }; return client; } @@ -102,9 +172,11 @@ export async function loginWithOpenIdSetup(returnUrl) { return { error: 'openid-setup-failed' }; } - const state = generators.state(); - const code_verifier = generators.codeVerifier(); - const code_challenge = generators.codeChallenge(code_verifier); + const state = openIdClient.randomState(); + const code_verifier = openIdClient.randomPKCECodeVerifier(); + const code_challenge = await openIdClient.calculatePKCECodeChallenge( + code_verifier, + ); const now_time = Date.now(); const expiry_time = now_time + 300 * 1000; @@ -171,7 +243,6 @@ export async function loginWithOpenIdFinalize(body) { try { let tokenSet = null; - if (!config.authMethod || config.authMethod === 'openid') { const params = { code: body.code, state: body.state }; tokenSet = await client.callback(client.redirect_uris[0], params, { @@ -184,9 +255,12 @@ export async function loginWithOpenIdFinalize(body) { code: body.code, redirect_uri: client.redirect_uris[0], code_verifier, + state: body.state, }); } + const userInfo = await client.userinfo(tokenSet.access_token); + const identity = userInfo.preferred_username ?? userInfo.login ?? @@ -207,7 +281,6 @@ export async function loginWithOpenIdFinalize(body) { ); if (countUsersWithUserName === 0) { userId = uuid.v4(); - // Check if user was created by another transaction const existingUser = accountDb.first( 'SELECT id FROM users WHERE user_name = ?', [identity], @@ -256,12 +329,11 @@ export async function loginWithOpenIdFinalize(body) { } else if (error.message === 'openid-grant-failed') { return { error: 'openid-grant-failed' }; } else { - throw error; // Re-throw other unexpected errors + throw error; } } const token = uuid.v4(); - let expiration; if (finalConfig.token_expiration === 'openid-provider') { expiration = tokenSet.expires_at ?? TOKEN_EXPIRATION_NEVER; @@ -314,7 +386,6 @@ export function isValidRedirectUrl(url) { try { const redirectUrl = new URL(url); const serverUrl = new URL(serverHostname); - if ( redirectUrl.hostname === serverUrl.hostname || redirectUrl.hostname === 'localhost' diff --git a/upcoming-release-notes/544.md b/upcoming-release-notes/544.md new file mode 100644 index 000000000..f4f654418 --- /dev/null +++ b/upcoming-release-notes/544.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [lelemm] +--- + +Bring openid-client lib to latest version diff --git a/yarn.lock b/yarn.lock index 54bcc6ef7..5eb183245 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1561,7 +1561,7 @@ __metadata: jws: "npm:^4.0.0" migrate: "npm:^2.0.1" nordigen-node: "npm:^1.4.0" - openid-client: "npm:^5.4.2" + openid-client: "npm:^6.1.7" prettier: "npm:^2.8.3" supertest: "npm:^6.3.1" typescript: "npm:^4.9.5" @@ -4255,10 +4255,10 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.15.5": - version: 4.15.9 - resolution: "jose@npm:4.15.9" - checksum: 10c0/4ed4ddf4a029db04bd167f2215f65d7245e4dc5f36d7ac3c0126aab38d66309a9e692f52df88975d99429e357e5fd8bab340ff20baab544d17684dd1d940a0f4 +"jose@npm:^5.9.6": + version: 5.9.6 + resolution: "jose@npm:5.9.6" + checksum: 10c0/d6bcd8c7d655b5cda8e182952a76f0c093347f5476d74795405bb91563f7ab676f61540310dd4b1531c60d685335ceb600571a409551d2cbd2ab3e9f9fbf1e4d languageName: node linkType: hard @@ -4475,15 +4475,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - "make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -4963,6 +4954,13 @@ __metadata: languageName: node linkType: hard +"oauth4webapi@npm:^3.1.4": + version: 3.1.4 + resolution: "oauth4webapi@npm:3.1.4" + checksum: 10c0/81e471750f4903121efcef4edb1b73d725ae6d3b9646a0febd45e29ed05b62faba14a69e433181eae441913684a02d681c4e561dcac578de9cb45dd719f53464 + languageName: node + linkType: hard + "object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -4970,13 +4968,6 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^2.2.0": - version: 2.2.0 - resolution: "object-hash@npm:2.2.0" - checksum: 10c0/1527de843926c5442ed61f8bdddfc7dc181b6497f725b0e89fcf50a55d9c803088763ed447cac85a5aa65345f1e99c2469ba679a54349ef3c4c0aeaa396a3eb9 - languageName: node - linkType: hard - "object-inspect@npm:^1.13.1": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" @@ -4984,13 +4975,6 @@ __metadata: languageName: node linkType: hard -"oidc-token-hash@npm:^5.0.3": - version: 5.0.3 - resolution: "oidc-token-hash@npm:5.0.3" - checksum: 10c0/d0dc0551406f09577874155cc83cf69c39e4b826293d50bb6c37936698aeca17d4bcee356ab910c859e53e83f2728a2acbd041020165191353b29de51fbca615 - languageName: node - linkType: hard - "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -5034,15 +5018,13 @@ __metadata: languageName: node linkType: hard -"openid-client@npm:^5.4.2": - version: 5.6.5 - resolution: "openid-client@npm:5.6.5" +"openid-client@npm:^6.1.7": + version: 6.1.7 + resolution: "openid-client@npm:6.1.7" dependencies: - jose: "npm:^4.15.5" - lru-cache: "npm:^6.0.0" - object-hash: "npm:^2.2.0" - oidc-token-hash: "npm:^5.0.3" - checksum: 10c0/4308dcd37a9ffb1efc2ede0bc556ae42ccc2569e71baa52a03ddfa44407bf403d4534286f6f571381c5eaa1845c609ed699a5eb0d350acfb8c3bacb72c2a6890 + jose: "npm:^5.9.6" + oauth4webapi: "npm:^3.1.4" + checksum: 10c0/195d26897bf6eb95cfd5b9e91e8bcac46c56b0570713cef6f74b78f9d3a23df0869b2f26e7d78ae6d511e8f3b50f33298aa1bdc7bc32a46d41ca7d4ea10df460 languageName: node linkType: hard