diff --git a/lib/utils/api/ErrorMedStatus.ts b/lib/utils/api/ErrorMedStatus.ts new file mode 100644 index 00000000..a4749cff --- /dev/null +++ b/lib/utils/api/ErrorMedStatus.ts @@ -0,0 +1,22 @@ +// TODO: Errorcode og developerMessage vil returneres fra innsending og oppslag etterhvert. +// Her må vi støtte begge deler parallelt i en periode. + +export class ErrorMedStatus extends Error { + status: number; + developerMessage?: string; + errorCode?: string; + navCallId?: string; + constructor( + message: string, + status: number, + navCallId = '', + developerMessage = '', + errorCode = '', + ) { + super(message); + this.status = status; + this.developerMessage = developerMessage; + this.errorCode = errorCode; + this.navCallId = navCallId; + } +} diff --git a/lib/utils/api/simpleTokenXProxy.ts b/lib/utils/api/simpleTokenXProxy.ts new file mode 100644 index 00000000..6ac0f965 --- /dev/null +++ b/lib/utils/api/simpleTokenXProxy.ts @@ -0,0 +1,86 @@ +import { validateToken, requestOboToken, getToken } from '@navikt/oasis'; +import { logError, logInfo } from '@navikt/aap-felles-utils'; +import { randomUUID } from 'crypto'; +import { IncomingMessage } from 'http'; +import { ErrorMedStatus } from 'lib/utils/api/ErrorMedStatus'; + +export const getOnBefalfOfToken = async ( + audience: string, + url: string, + req: IncomingMessage, +): Promise => { + const token = getToken(req); + if (!token) { + logError(`Token for ${url} er undefined`); + throw new Error('Token for simpleTokenXProxy is undefined'); + } + + const validation = await validateToken(token); + if (!validation.ok) { + logError(`Token for ${url} validerte ikke`); + throw new Error('Token for simpleTokenXProxy didnt validate'); + } + + const onBehalfOf = await requestOboToken(token, audience); + if (!onBehalfOf.ok) { + logError(`Henting av oboToken for ${url} feilet`); + throw new Error('Request oboToken for simpleTokenXProxy failed'); + } + + return onBehalfOf.token; +}; + +interface Opts { + url: string; + method?: 'GET' | 'POST' | 'DELETE'; + audience: string; + body?: object; + req?: IncomingMessage; +} + +export const simpleTokenXProxy = async ({ + url, + audience, + req, + method = 'GET', + body, +}: Opts): Promise => { + if (!req) { + logError(`Request for ${url} er undefined`); + throw new Error('Request for simpleTokenXProxy is undefined'); + } + const onBehalfOfToken = await getOnBefalfOfToken(audience, url, req); + const navCallId = randomUUID(); + + logInfo(`${req.method} ${url}, callId ${navCallId}`); + + const response = await fetch(url, { + method: method, + headers: { + Authorization: `Bearer ${onBehalfOfToken}`, + 'Content-Type': 'application/json', + 'Nav-CallId': navCallId, + }, + body: method === 'POST' ? JSON.stringify(body) : undefined, + }); + + try { + if (response.ok) { + logInfo(`OK ${url}, status ${response.status}, callId ${navCallId}`); + const headers = response.headers.get('content-type'); + const isJson = headers?.includes('application/json'); + + // TODO: Midlertidig, til innsending returnerer json på alle OK-responser + if (!isJson) { + return (await response.text()) as T; + } + return await response.json(); + } + } catch (error) { + logError(`Unable to parse response for ${url}`, error); + } + logError( + `Error fetching simpleTokenXProxy. Fikk responskode ${response.status} fra ${url} med navCallId: ${navCallId}`, + ); + throw new ErrorMedStatus('Error fetching simpleTokenXProxy', response.status, navCallId); +}; diff --git a/package.json b/package.json index c2cfe6ac..7b990d12 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@navikt/ds-react": "^5.17.4", "@navikt/nav-dekoratoren-moduler": "^2.1.5", "@navikt/next-api-proxy": "3.4.0", + "@navikt/oasis": "^3.2.2", "@ungap/structured-clone": "^1.2.0", "@vercel/otel": "1.2.1", "cross-fetch": "^4.0.0", diff --git a/pages/[step].tsx b/pages/[step].tsx index 1ec2c9a0..cae5489e 100644 --- a/pages/[step].tsx +++ b/pages/[step].tsx @@ -234,7 +234,7 @@ export const getServerSideProps = beskyttetSide( let mellomlagretSøknad: SoknadContextState | undefined; try { - mellomlagretSøknad = await hentMellomlagring(bearerToken); + mellomlagretSøknad = await hentMellomlagring(ctx.req); } catch (e) { logError('Noe gikk galt i innhenting av mellomlagret søknad', e); } diff --git a/pages/api/innsending/soknadinnsending.ts b/pages/api/innsending/soknadinnsending.ts index 56d29642..1ab26727 100644 --- a/pages/api/innsending/soknadinnsending.ts +++ b/pages/api/innsending/soknadinnsending.ts @@ -18,6 +18,9 @@ import { getYrkesskadeSchema } from 'components/pageComponents/standard/Yrkesska import { getAccessTokenFromRequest } from 'auth/accessToken'; import { AttachmentType, RequiredVedlegg } from 'types/SoknadContext'; import { SOKNAD_VERSION } from 'context/soknadcontext/soknadContext'; +import { deleteCache } from 'mock/mellomlagringsCache'; +import { simpleTokenXProxy } from 'lib/utils/api/simpleTokenXProxy'; +import { IncomingMessage } from 'http'; // TODO: Sjekke om vi må generere pdf på samme språk som bruker har valgt når de fyller ut søknaden function getIntl() { @@ -67,8 +70,6 @@ const søknadIsValid = (søknad: Soknad) => { }; const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) => { - const accessToken = getAccessTokenFromRequest(req); - const { søknad, requiredVedlegg } = req.body as { søknad: Soknad; requiredVedlegg: RequiredVedlegg[]; @@ -106,7 +107,7 @@ const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) = kvittering: søknadPdf, filer, }, - accessToken, + req, ); metrics.sendSoknadCounter.inc({ type: 'STANDARD' }); @@ -145,26 +146,28 @@ function mapVedleggTypeTilVedleggTekst(vedleggType: AttachmentType): string { export const sendSoknadViaAapInnsending = async ( innsending: SoknadInnsendingRequestBody, - accessToken?: string, + req: IncomingMessage, ) => { if (isFunctionalTest()) { return 'Vi har mottat søknaden din.'; } if (isMock()) { + await deleteCache(); return 'Vi har mottat søknaden din.'; } - const søknad = await tokenXApiProxy({ - url: `${process.env.INNSENDING_URL}/innsending`, - prometheusPath: 'innsending/soknad', - method: 'POST', - data: JSON.stringify(innsending), - audience: process.env.INNSENDING_AUDIENCE!, - bearerToken: accessToken, - metricsStatusCodeCounter: metrics.backendApiStatusCodeCounter, - metricsTimer: metrics.backendApiDurationHistogram, - noResponse: true, - }); - return søknad; + try { + const søknad = await simpleTokenXProxy({ + url: `${process.env.INNSENDING_URL}/innsending`, + audience: process.env.INNSENDING_AUDIENCE!, + method: 'POST', + body: innsending, + req, + }); + return søknad; + } catch (error) { + logError('Noe gikk galt ved innsending av søknad', error); + throw new Error('Error sending søknad via aap-innsending'); + } }; export default handler; diff --git a/pages/api/mellomlagring/lagre.ts b/pages/api/mellomlagring/lagre.ts index 7b9db328..c4a2f3b7 100644 --- a/pages/api/mellomlagring/lagre.ts +++ b/pages/api/mellomlagring/lagre.ts @@ -8,11 +8,11 @@ import metrics from 'utils/metrics'; import { StepType } from 'components/StepWizard/Step'; import { hentMellomlagring } from 'pages/api/mellomlagring/les'; +import { simpleTokenXProxy } from 'lib/utils/api/simpleTokenXProxy'; +import { IncomingMessage } from 'http'; const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) => { - const accessToken = getAccessTokenFromRequest(req); - - const eksisterendeSøknad = await hentMellomlagring(accessToken); + const eksisterendeSøknad = await hentMellomlagring(req); if ( eksisterendeSøknad && eksisterendeSøknad.søknad && @@ -27,25 +27,26 @@ const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) = `Overskriver eksisterende søknad med en tom søknad på side ${activeStepIndex ?? 'ukjent'}`, ); } - await lagreBucket(req.body, accessToken); + await mellomlagreSøknad(req.body, req); res.status(201).json({}); }); -export const lagreBucket = async (data: string, accessToken?: string) => { +export const mellomlagreSøknad = async (data: object, req: IncomingMessage) => { if (isFunctionalTest()) return; if (isMock()) return await lagreCache(JSON.stringify(data)); - await tokenXApiProxy({ - url: `${process.env.INNSENDING_URL}/mellomlagring/søknad`, - prometheusPath: `mellomlagring`, - method: 'POST', - data: JSON.stringify(data), - audience: process.env.INNSENDING_AUDIENCE!, - noResponse: true, - bearerToken: accessToken, - metricsStatusCodeCounter: metrics.backendApiStatusCodeCounter, - metricsTimer: metrics.backendApiDurationHistogram, - }); - return; + try { + await simpleTokenXProxy({ + url: `${process.env.INNSENDING_URL}/mellomlagring/søknad`, + method: 'POST', + audience: process.env.INNSENDING_AUDIENCE!, + body: data, + req, + }); + return; + } catch (error) { + logError('Noe gikk galt ved mellomlagring av søknad', error); + throw new Error('Error saving søknad via aap-innsending'); + } }; export default handler; diff --git a/pages/api/mellomlagring/les.ts b/pages/api/mellomlagring/les.ts index f4cf5671..c8e11b41 100644 --- a/pages/api/mellomlagring/les.ts +++ b/pages/api/mellomlagring/les.ts @@ -7,15 +7,16 @@ import { lesCache } from 'mock/mellomlagringsCache'; import { isFunctionalTest, isMock } from 'utils/environments'; import { defaultStepList } from 'pages'; import { SOKNAD_VERSION, SoknadContextState } from 'context/soknadcontext/soknadContext'; +import { simpleTokenXProxy } from 'lib/utils/api/simpleTokenXProxy'; +import { IncomingMessage } from 'http'; const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) => { - const accessToken = getAccessTokenFromRequest(req); - const result = await hentMellomlagring(accessToken); + const result = await hentMellomlagring(req); res.status(200).json(result); }); export const hentMellomlagring = async ( - accessToken?: string, + req?: IncomingMessage, ): Promise => { if (isFunctionalTest()) { return { @@ -30,14 +31,11 @@ export const hentMellomlagring = async ( return result ? JSON.parse(result) : {}; } try { - const mellomlagretSøknad = await tokenXApiProxy({ + const mellomlagretSøknad = await simpleTokenXProxy({ url: `${process.env.INNSENDING_URL}/mellomlagring/søknad`, - prometheusPath: `mellomlagring`, method: 'GET', audience: process.env.INNSENDING_AUDIENCE!, - bearerToken: accessToken, - metricsStatusCodeCounter: metrics.backendApiStatusCodeCounter, - metricsTimer: metrics.backendApiDurationHistogram, + req, }); return mellomlagretSøknad; diff --git a/pages/api/mellomlagring/slett.ts b/pages/api/mellomlagring/slett.ts index 8b6ce975..e9beacef 100644 --- a/pages/api/mellomlagring/slett.ts +++ b/pages/api/mellomlagring/slett.ts @@ -1,33 +1,37 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getAccessTokenFromRequest } from 'auth/accessToken'; import { beskyttetApi } from 'auth/beskyttetApi'; -import { tokenXApiProxy } from '@navikt/aap-felles-utils'; +import { logError, tokenXApiProxy } from '@navikt/aap-felles-utils'; import metrics from 'utils/metrics'; import { deleteCache } from 'mock/mellomlagringsCache'; import { isMock } from 'utils/environments'; +import { simpleTokenXProxy } from 'lib/utils/api/simpleTokenXProxy'; +import { IncomingMessage } from 'http'; +import { ca } from 'date-fns/locale'; const handler = beskyttetApi(async (req: NextApiRequest, res: NextApiResponse) => { - const accessToken = getAccessTokenFromRequest(req); - await slettBucket(accessToken); + await slettBucket(req); res.status(204).json({}); }); -export const slettBucket = async (accessToken?: string) => { +export const slettBucket = async (req: IncomingMessage) => { if (isMock()) { await deleteCache(); return; } - await tokenXApiProxy({ - url: `${process.env.INNSENDING_URL}/mellomlagring/søknad`, - prometheusPath: `mellomlagring`, - method: 'DELETE', - noResponse: true, - audience: process.env.INNSENDING_AUDIENCE!, - bearerToken: accessToken, - metricsStatusCodeCounter: metrics.backendApiStatusCodeCounter, - metricsTimer: metrics.backendApiDurationHistogram, - }); + try { + await simpleTokenXProxy({ + url: `${process.env.INNSENDING_URL}/mellomlagring/søknad`, + method: 'DELETE', + audience: process.env.INNSENDING_AUDIENCE!, + req, + }); + return; + } catch (error) { + logError('Noe gikk galt ved sletting av søknad', error); + throw new Error('Error deleting søknad via aap-innsending'); + } }; export default handler; diff --git a/pages/index.tsx b/pages/index.tsx index 2997c4ad..5d42bf09 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -113,7 +113,7 @@ export const getServerSideProps = beskyttetSide( let mellomlagretSøknad: SoknadContextState | undefined; try { - mellomlagretSøknad = await hentMellomlagring(bearerToken); + mellomlagretSøknad = await hentMellomlagring(ctx.req); } catch (e) { logError('Noe gikk galt i innhenting av mellomlagret søknad', e); } diff --git a/yarn.lock b/yarn.lock index 53f0720e..7fd79029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1378,6 +1378,17 @@ __metadata: languageName: node linkType: hard +"@navikt/oasis@npm:^3.2.2": + version: 3.2.2 + resolution: "@navikt/oasis@npm:3.2.2::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40navikt%2Foasis%2F3.2.2%2F388817639184717074cfa3a83cdf085b17193eea" + dependencies: + jose: "npm:^5.2.2" + openid-client: "npm:^5.6.4" + prom-client: "npm:^15.1.0" + checksum: 10c0/58bf39b742953188c1349694ad657b7a0694c49c9c88224ef866f4bfe21609006076bd4661e2fd26d5f908740ab5f5b8dde81c0e97cbcae41d4adf109a22ee30 + languageName: node + linkType: hard + "@next/env@npm:14.1.0": version: 14.1.0 resolution: "@next/env@npm:14.1.0" @@ -1522,6 +1533,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:^1.4.0": + version: 1.8.0 + resolution: "@opentelemetry/api@npm:1.8.0" + checksum: 10c0/66d5504bfbf9c19a14ea549f5fca975a73a5e1e8a1e40a6dc2d662893c942b9ba66c009262816dee2b9ffd0267acd707ec692eba20db11a09d4ee114c00dc161 + languageName: node + linkType: hard + "@opentelemetry/api@npm:^1.7.0": version: 1.7.0 resolution: "@opentelemetry/api@npm:1.7.0" @@ -2848,6 +2866,7 @@ __metadata: "@navikt/ds-react": "npm:^5.17.4" "@navikt/nav-dekoratoren-moduler": "npm:^2.1.5" "@navikt/next-api-proxy": "npm:3.4.0" + "@navikt/oasis": "npm:^3.2.2" "@playwright/test": "npm:^1.41.2" "@testing-library/dom": "npm:^9.3.4" "@testing-library/jest-dom": "npm:^6.4.1" @@ -6540,6 +6559,20 @@ __metadata: languageName: node linkType: hard +"jose@npm:^4.15.5": + version: 4.15.5 + resolution: "jose@npm:4.15.5" + checksum: 10c0/9f208492f55ae9c547fd407c36f67ec3385051b5ca390e24f5449740f17359640b3f96fabfd38bc132cc4292b964c31b921bf356253373b1bd3eb6df799b7433 + languageName: node + linkType: hard + +"jose@npm:^5.2.2": + version: 5.2.3 + resolution: "jose@npm:5.2.3" + checksum: 10c0/7cf02e1d1d6226b6ee136fb6c53fd4dde9cfdaf1613ceaab3a5629803eaa80cbfd77cddc38a54c55c82b8f63428677660c93fc87493818a07adc9c0c77ef16ff + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -7378,7 +7411,7 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^2.0.1": +"object-hash@npm:^2.0.1, object-hash@npm:^2.2.0": version: 2.2.0 resolution: "object-hash@npm:2.2.0" checksum: 10c0/1527de843926c5442ed61f8bdddfc7dc181b6497f725b0e89fcf50a55d9c803088763ed447cac85a5aa65345f1e99c2469ba679a54349ef3c4c0aeaa396a3eb9 @@ -7500,6 +7533,13 @@ __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-exit-leak-free@npm:^2.1.0": version: 2.1.0 resolution: "on-exit-leak-free@npm:2.1.0" @@ -7556,6 +7596,18 @@ __metadata: languageName: node linkType: hard +"openid-client@npm:^5.6.4": + version: 5.6.5 + resolution: "openid-client@npm:5.6.5" + 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 + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -7980,6 +8032,16 @@ __metadata: languageName: node linkType: hard +"prom-client@npm:^15.1.0": + version: 15.1.0 + resolution: "prom-client@npm:15.1.0" + dependencies: + "@opentelemetry/api": "npm:^1.4.0" + tdigest: "npm:^0.1.1" + checksum: 10c0/c10781adbf49225298e44da5396a51a0bd4d0cddc3c7e237ba50e888e12ead26a8f98261f362a442f1bbcdaddd6e7302d5675b37beac67ea9b6f82e4d39fb3cc + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1"