From 4b8d94fa303d0c198a3ac5484490800de2fd29c7 Mon Sep 17 00:00:00 2001 From: Alex Layton Date: Mon, 4 Mar 2024 18:01:55 -0500 Subject: [PATCH] feat(auth): progress towards auth0/oidc support --- oada/libs/lib-config/package.json | 2 +- oada/libs/pino-debug/package.json | 2 +- oada/oada.config.mjs | 3 +- oada/package.json | 4 +- oada/services/auth/package.json | 3 +- oada/services/auth/src/auth.ts | 24 ++- oada/services/auth/src/config.ts | 25 ++- oada/services/auth/src/db/arango/codes.ts | 45 ----- oada/services/auth/src/db/arango/tokens.ts | 51 ----- oada/services/auth/src/db/flat/codes.ts | 33 ---- oada/services/auth/src/db/flat/tokens.ts | 33 ---- oada/services/auth/src/db/models/code.ts | 158 --------------- oada/services/auth/src/db/models/token.ts | 123 ------------ oada/services/auth/src/index.ts | 39 +++- oada/services/auth/src/oauth2.ts | 150 ++++++++++---- oada/services/auth/src/oidc.ts | 97 +++++---- oada/services/http-handler/package.json | 4 +- .../http-handler/src/authorizations.ts | 10 +- oada/services/http-handler/src/resources.ts | 12 +- oada/services/http-handler/src/server.ts | 47 +++-- oada/services/http-handler/src/users.ts | 2 +- oada/services/http-handler/tsconfig.json | 1 + .../permissions-handler/src/server.ts | 4 +- oada/services/well-known/src/config.ts | 6 +- oada/services/well-known/src/index.ts | 12 +- oada/yarn.lock | 186 +++++++++++------- 26 files changed, 415 insertions(+), 661 deletions(-) delete mode 100644 oada/services/auth/src/db/arango/codes.ts delete mode 100644 oada/services/auth/src/db/arango/tokens.ts delete mode 100644 oada/services/auth/src/db/flat/codes.ts delete mode 100644 oada/services/auth/src/db/flat/tokens.ts delete mode 100644 oada/services/auth/src/db/models/code.ts delete mode 100644 oada/services/auth/src/db/models/token.ts diff --git a/oada/libs/lib-config/package.json b/oada/libs/lib-config/package.json index 57c0c8a8..fcc8a221 100644 --- a/oada/libs/lib-config/package.json +++ b/oada/libs/lib-config/package.json @@ -38,7 +38,7 @@ "convict-format-with-moment": "^6.2.0", "convict-format-with-validator": "^6.2.0", "debug": "^4.3.4", - "dotenv": "^16.4.4", + "dotenv": "^16.4.5", "json5": "^2.2.3", "tslib": "2.6.2", "yaml": "^2.3.4" diff --git a/oada/libs/pino-debug/package.json b/oada/libs/pino-debug/package.json index c4d7a6f4..467ee05f 100644 --- a/oada/libs/pino-debug/package.json +++ b/oada/libs/pino-debug/package.json @@ -25,7 +25,7 @@ "dependencies": { "cls-rtracer": "^2.6.3", "is-interactive": "^2.0.0", - "pino": "^8.18.0", + "pino": "^8.19.0", "pino-caller": "^3.4.0", "pino-debug": "^2.0.0", "pino-loki": "^2.2.1", diff --git a/oada/oada.config.mjs b/oada/oada.config.mjs index 24e010fa..cca81ace 100644 --- a/oada/oada.config.mjs +++ b/oada/oada.config.mjs @@ -162,10 +162,9 @@ export default { domain, }, 'mergeSubServices': [ - { resource: 'oada-configuration', base: 'http://auth:8080' }, { resource: 'openid-configuration', base: 'http://auth:8080' }, ], - 'oada-configuration': { + 'openid-configuration': { // eslint-disable-next-line camelcase well_known_version: '1.1.0', // eslint-disable-next-line camelcase diff --git a/oada/package.json b/oada/package.json index 94d8633f..c159b416 100644 --- a/oada/package.json +++ b/oada/package.json @@ -19,8 +19,8 @@ "@types/eslint": "^8.56.2", "@types/mocha": "^10.0.6", "@types/node": "^20.11.19", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", "@yarnpkg/sdks": "^3.1.0", "browserslist": "^4.23.0", "c8": "^9.1.0", diff --git a/oada/services/auth/package.json b/oada/services/auth/package.json index 71953f00..d3854642 100644 --- a/oada/services/auth/package.json +++ b/oada/services/auth/package.json @@ -52,13 +52,14 @@ "@fastify/cors": "^9.0.1", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", + "@fastify/jwt": "^8.0.0", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.1.0", "@fastify/request-context": "^5.1.0", "@fastify/secure-session": "^7.1.0", "@fastify/sensible": "^5.5.0", "@fastify/static": "^7.0.1", - "@fastify/view": "^8.2.0", + "@fastify/view": "^9.0.0", "@oada/certs": "^4.1.1", "@oada/error": "^2.0.1", "@oada/lib-arangodb": "^3.7.0", diff --git a/oada/services/auth/src/auth.ts b/oada/services/auth/src/auth.ts index 02fc0051..408c3847 100644 --- a/oada/services/auth/src/auth.ts +++ b/oada/services/auth/src/auth.ts @@ -15,13 +15,16 @@ * limitations under the License. */ +import { + Strategy as BearerStrategy, + type VerifyFunctionWithRequest, +} from 'passport-http-bearer'; import { Strategy as JWTStrategy, type VerifyCallbackWithRequest, } from 'passport-jwt'; import { type RSA_JWK, jwk2pem } from 'pem-jwk'; import { Authenticator } from '@fastify/passport'; -import { Strategy as BearerStrategy } from 'passport-http-bearer'; import ClientPassword from 'passport-oauth2-client-password'; import { Strategy as LocalStrategy } from 'passport-local'; import debug from 'debug'; @@ -34,9 +37,10 @@ import { findByUsernamePassword, findById as findUserById, } from './db/models/user.js'; +import type { FastifyRequest } from 'fastify'; import { _defaultHack } from './index.js'; import { findById } from './db/models/client.js'; -import { findByToken } from './db/models/token.js'; +import { verifyToken } from './oauth2.js'; export const fastifyPassport = new Authenticator({ clearSessionOnLogin: process.env.NODE_ENV === 'development', @@ -208,19 +212,25 @@ fastifyPassport.use( // BearerStrategy used to protect userinfo endpoint fastifyPassport.use( 'bearer', - new BearerStrategy(async (token, done) => { + new BearerStrategy({}, (async (request, token, done) => { try { - const t = await findByToken(token); - if (!t) { + const issuer = `${request.protocol}://${request.hostname}/` as const; + const payload = await verifyToken(issuer, token); + (request as unknown as FastifyRequest).log.debug( + { issuer, jwt: token, payload }, + 'JWT Bearer token verify', + ); + + if (!payload) { // eslint-disable-next-line unicorn/no-null done(null, false); return; } // eslint-disable-next-line unicorn/no-null - done(null, t.user, { scope: t.scope.slice() }); + done(null, payload.user, { scope: [...payload.scope] }); } catch (error: unknown) { done(error); } - }), + }) satisfies VerifyFunctionWithRequest), ); diff --git a/oada/services/auth/src/config.ts b/oada/services/auth/src/config.ts index b08e82f7..b825fc4c 100644 --- a/oada/services/auth/src/config.ts +++ b/oada/services/auth/src/config.ts @@ -265,6 +265,11 @@ export const { config, schema } = await libConfig({ nullable: true, default: null as unknown as Promise, }, + alg: { + doc: 'Algorithm to use for encrypting codes', + format: String, + default: 'HS256' as 'HS256' | 'RS256' | 'PS256', + }, pkce: { required: { format: Boolean, @@ -277,14 +282,21 @@ export const { config, schema } = await libConfig({ }, }, token: { - length: { - format: 'nat', - default: 40, - }, expiresIn: { format: 'duration', default: 0, }, + key: { + doc: 'Key to use for signing tokens', + format: 'file-url', + nullable: true, + default: null as unknown as Promise, + }, + alg: { + doc: 'Algorithm to use for signing tokens', + format: String, + default: 'RS256' as 'HS256' | 'RS256' | 'PS256', + }, }, idToken: { expiresIn: { @@ -297,6 +309,11 @@ export const { config, schema } = await libConfig({ nullable: true, default: null as unknown as Promise, }, + alg: { + doc: 'Algorithm to use for signing id tokens', + format: String, + default: 'RS256' as 'HS256' | 'RS256' | 'PS256', + }, }, certs: { // If you want to run in https mode you need certs here. diff --git a/oada/services/auth/src/db/arango/codes.ts b/oada/services/auth/src/db/arango/codes.ts deleted file mode 100644 index af15dc52..00000000 --- a/oada/services/auth/src/db/arango/codes.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright 2017-2021 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -import { codes } from '@oada/lib-arangodb'; - -import type { ICode } from '../models/code.js'; - -const trace = debug('arango:codes:trace'); - -export async function findByCode(code: string): Promise { - trace('findByCode: searching for code %s', code); - const found = await codes.findByCode(code); - if (!found) { - return found; - } - - const { _id, ...c } = found; - return { ...c, id: _id, user: { id: c.user._id } }; -} - -export async function save(code: ICode) { - const { id, user, ...c } = code; - return codes.save({ - ...c, - _id: id, - // Link user - user: { _id: user.id }, - }); -} diff --git a/oada/services/auth/src/db/arango/tokens.ts b/oada/services/auth/src/db/arango/tokens.ts deleted file mode 100644 index a31e65c2..00000000 --- a/oada/services/auth/src/db/arango/tokens.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2017-2021 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -import { authorizations } from '@oada/lib-arangodb'; - -import type { IToken } from '../models/token.js'; - -const trace = debug('arango:token:trace'); - -export async function findByToken(token: string) { - trace('findByToken: searching for token %s', token); - const found = await authorizations.findByToken(token); - if (!found) { - return found; - } - - const { _id, ...t } = found; - if (t?.user) { - // Why eliminate the _id? - // Object.assign(t.user, {id: t.user._id, _id: undefined}); - } - - return { ...t, id: _id }; -} - -export async function save({ id, ...token }: IToken) { - const t = { - ...token, - _id: id, - // Link user - user: { _id: token.user.id }, - }; - trace(t, 'save: saving token'); - return authorizations.save(t); -} diff --git a/oada/services/auth/src/db/flat/codes.ts b/oada/services/auth/src/db/flat/codes.ts deleted file mode 100644 index 9c162d63..00000000 --- a/oada/services/auth/src/db/flat/codes.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2017-2022 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import cloneDeep from 'clone-deep'; - -import type { Code } from '../models/code.js'; -// @ts-expect-error IDEK -import codes from './codes.json'; - -const database = new Map(Object.entries(codes as Record)); - -export function findByCode(code: string) { - return cloneDeep(database.get(code)); -} - -export function save(code: Code) { - database.set(code.code, cloneDeep(code)); - return findByCode(code.code); -} diff --git a/oada/services/auth/src/db/flat/tokens.ts b/oada/services/auth/src/db/flat/tokens.ts deleted file mode 100644 index 36d8211d..00000000 --- a/oada/services/auth/src/db/flat/tokens.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2017-2022 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import cloneDeep from 'clone-deep'; - -import type { Token } from '../models/token.js'; -// @ts-expect-error IDEK -import tokens from './tokens.json'; - -const database = new Map(Object.entries(tokens as Record)); - -export function findByToken(token: string) { - return cloneDeep(database.get(token)); -} - -export function save(token: Token) { - database.set(token.token, cloneDeep(token)); - return findByToken(token.token); -} diff --git a/oada/services/auth/src/db/models/code.ts b/oada/services/auth/src/db/models/code.ts deleted file mode 100644 index 6de9bae0..00000000 --- a/oada/services/auth/src/db/models/code.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @license - * Copyright 2017-2021 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { config } from '../../config.js'; - -import path from 'node:path'; -import url from 'node:url'; - -import debug from 'debug'; - -import { Codes, OADAError } from '@oada/error'; -import type { CodeID } from '@oada/lib-arangodb/dist/libs/codes.js'; -import type { UserID } from '@oada/lib-arangodb/dist/libs/users.js'; - -const trace = debug('model-codes:trace'); - -export interface ICodes { - findByCode(code: Code['code']): Promise; - save(code: Code): Promise; -} - -const dirname = path.dirname(url.fileURLToPath(import.meta.url)); -const { datastoresDriver } = config.get('auth'); -const database = (await import( - path.join(dirname, '..', datastoresDriver, 'codes.js') -)) as ICodes; - -export interface ICode { - readonly id?: CodeID; - readonly code: string; - readonly nonce?: string; - readonly scope?: readonly string[]; - readonly user: { readonly id: UserID }; - readonly clientId: string; - readonly createTime?: number; - readonly expiresIn?: number; - readonly redeemed?: boolean; - readonly redirectUri: string; -} -export class Code implements ICode { - readonly id; - readonly code; - readonly nonce; - readonly scope; - readonly user; - readonly clientId; - readonly createTime; - readonly expiresIn; - redeemed; - readonly redirectUri; - - constructor({ - id, - code, - nonce, - scope = [], - user, - clientId, - createTime = Date.now(), - expiresIn = 60, - redeemed = false, - redirectUri, - }: ICode) { - this.id = id; - this.code = code; - this.nonce = nonce; - this.scope = scope; - this.user = user; - this.clientId = clientId; - this.createTime = createTime; - this.expiresIn = expiresIn; - this.redeemed = redeemed; - this.redirectUri = redirectUri; - - if (!this.isValid()) { - throw new Error('Invalid code'); - } - } - - isValid() { - return ( - typeof this.code === 'string' && - Array.isArray(this.scope) && - typeof this.user === 'object' && - typeof this.clientId === 'string' && - typeof this.redirectUri === 'string' - ); - } - - isExpired() { - return this.createTime + this.expiresIn > Date.now(); - } - - matchesClientId(clientId: string) { - return this.clientId === clientId; - } - - matchesRedirectUri(redirectUri: string) { - return this.redirectUri === redirectUri; - } - - isRedeemed() { - return this.redeemed; - } - - async redeem() { - this.redeemed = true; - - trace('makeCode#redeem: saving redeemed code ', this.code); - await database.save(this); - - const redeemed = await findByCode(this.code); - if (!redeemed) { - throw new Error('Could not redeem code'); - } - - return redeemed; - } -} - -export async function findByCode(code: Code['code']) { - const c = await database.findByCode(code); - return c ? new Code(c) : undefined; -} - -export async function save(c: ICode) { - const code = c instanceof Code ? c : new Code(c); - - if (await database.findByCode(code.code)) { - throw new OADAError( - 'Code already exists', - Codes.BadRequest, - 'There was a problem durring the login', - ); - } - - await database.save(code); - const saved = await findByCode(code.code); - if (!saved) { - throw new Error('Could not save code'); - } - - return saved; -} diff --git a/oada/services/auth/src/db/models/token.ts b/oada/services/auth/src/db/models/token.ts deleted file mode 100644 index 074f3adb..00000000 --- a/oada/services/auth/src/db/models/token.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2017-2021 Open Ag Data Alliance - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { config } from '../../config.js'; - -import path from 'node:path'; -import url from 'node:url'; - -import debug from 'debug'; - -import { Codes, OADAError } from '@oada/error'; - -const log = debug('model-tokens'); - -export interface ITokens { - findByToken(token: string): Promise; - save(token: Token): Promise; -} - -const dirname = path.dirname(url.fileURLToPath(import.meta.url)); -const { datastoresDriver } = config.get('auth'); -const database = (await import( - path.join(dirname, '..', datastoresDriver, 'tokens.js') -)) as ITokens; - -export interface IToken { - readonly id?: string; - readonly token: string; - readonly scope?: readonly string[]; - readonly user: { readonly id: string }; - readonly clientId: string; - readonly createTime?: number; - readonly expiresIn?: number; -} -export class Token implements IToken { - readonly id; - readonly token; - readonly scope; - readonly user; - readonly clientId; - readonly createTime; - readonly expiresIn; - - constructor({ - id, - token, - scope = [], - user, - clientId, - createTime = Date.now(), - expiresIn = 60, - }: IToken) { - this.id = id; - this.token = token!; - this.clientId = clientId!; - this.scope = scope; - this.user = user; - this.createTime = createTime; - this.expiresIn = expiresIn; - - if (!this.isValid()) { - throw new Error('Invalid token'); - } - } - - isValid() { - return ( - typeof this.token === 'string' && - Array.isArray(this.scope) && - typeof this.user === 'object' && - typeof this.clientId === 'string' && - typeof this.expiresIn === 'number' - ); - } - - isExpired() { - return this.createTime + this.expiresIn < Date.now(); - } -} - -export async function findByToken(token: string) { - const t = await database.findByToken(token); - if (t) { - return new Token(t); - } -} - -export async function save(t: IToken) { - const token = t instanceof Token ? t : new Token(t); - - const tok = await findByToken(token.token); - if (tok) { - throw new OADAError( - 'Token already exists', - Codes.BadRequest, - 'There was a problem login in', - ); - } - - log(token, 'save: saving token '); - await database.save(token); - - const saved = await findByToken(token.token); - if (!saved) { - throw new Error('Could not save token'); - } - - return saved; -} diff --git a/oada/services/auth/src/index.ts b/oada/services/auth/src/index.ts index 6e1c919e..6af81528 100644 --- a/oada/services/auth/src/index.ts +++ b/oada/services/auth/src/index.ts @@ -21,6 +21,7 @@ import { config, domainConfigs } from './config.js'; import '@oada/lib-prom'; +import { join } from 'node:path/posix'; import path from 'node:path'; import { randomBytes } from 'node:crypto'; import url from 'node:url'; @@ -303,14 +304,20 @@ export async function start(): Promise { req(request: FastifyRequest) { const version = request.headers?.['accept-version']; return { - method: request.method, - url: request.url, - version: version ? `${version}` : undefined, - hostname: request.hostname, - userAgent: request.headers?.['user-agent'], - remoteAddress: request.ip, - remotePort: request.socket?.remotePort, - session: request.session?.data(), + 'method': request.method, + 'url': request.url, + 'version': version ? `${version}` : undefined, + 'hostname': request.hostname, + 'userAgent': request.headers?.['user-agent'], + 'remoteAddress': request.ip, + 'remotePort': request.socket?.remotePort, + 'forwarded': request.headers?.forwarded, + 'x-forwarded': { + host: request.headers?.['x-forwarded-host'], + proto: request.headers?.['x-forwarded-proto'], + for: request.headers?.['x-forwarded-for'], + }, + 'session': request.session?.data(), }; }, // Customize logging for responses @@ -343,9 +350,21 @@ export async function start(): Promise { try { const port = config.get('auth.server.port'); + const prefix = config.get('auth.endpointsPrefix'); await fastify.register(plugin, { - prefix: config.get('auth.endpointsPrefix'), + prefix, }); + if (prefix) { + // HACK: make root .well-known redirect to our auth prefix + fastify.all<{ Params: { document: string } }>( + '/.well-known/:document', + (request, reply) => + reply.redirect( + 301, + join(prefix, '.well-known', request.params.document), + ), + ); + } fastify.log.info('OADA server starting on port %d', port); await fastify.listen({ @@ -374,3 +393,5 @@ if (esMain(import.meta)) { } export default start; + +export type { TokenClaims } from './oauth2.js'; diff --git a/oada/services/auth/src/oauth2.ts b/oada/services/auth/src/oauth2.ts index e6118444..0482307c 100644 --- a/oada/services/auth/src/oauth2.ts +++ b/oada/services/auth/src/oauth2.ts @@ -17,19 +17,30 @@ import { config, domainConfigs } from './config.js'; -import { createHash, randomBytes } from 'node:crypto'; +import { + createHash, + createPrivateKey, + createPublicKey, + randomBytes, +} from 'node:crypto'; import type { ServerResponse } from 'node:http'; import { promisify } from 'node:util'; import type {} from '@fastify/formbody'; -import type { FastifyPluginAsync, FastifyReply } from 'fastify'; +import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'; import { fastifyPassport } from './auth.js'; import { EncryptJWT, type JWTDecryptResult, + type JWTPayload, + SignJWT, + createLocalJWKSet, + exportJWK, + generateKeyPair, generateSecret, jwtDecrypt, + jwtVerify, } from 'jose'; import oauth2orize, { AuthorizationError, @@ -50,7 +61,6 @@ import type { DBUser, User } from './db/models/user.js'; import type { Client } from './db/models/client.js'; import { findById } from './db/models/client.js'; import { promisifyMiddleware } from './utils.js'; -import { save as saveToken } from './db/models/token.js'; // If the array of scopes contains ONLY openid OR openid and profile, auto-accept. // Better to handle this by asking only the first time, but this is quicker to get PoC working. @@ -74,20 +84,15 @@ function scopeIsOnlyOpenid(scopes: string | readonly string[]): boolean { ); } -function makeHash(length: number) { - return randomBytes(Math.ceil((length * 3) / 4)) - .toString('base64') - .slice(0, length) - .replaceAll('+', '-') - .replaceAll('/', '_') - .replaceAll('=', ''); -} - declare module 'oauth2orize' { // eslint-disable-next-line @typescript-eslint/no-shadow interface OAuth2Req { nonce?: string; + issuer: string; } + + // eslint-disable-next-line @typescript-eslint/no-shadow + interface MiddlewareRequest extends FastifyRequest {} } interface DeserializedOauth2 extends OAuth2 { client: C; @@ -106,37 +111,105 @@ export interface Options { }; } +export const kid = 'oauth2-1' as const; +// eslint-disable-next-line @typescript-eslint/ban-types +export async function getKeyPair(file: File | null, alg: string) { + if (!file) { + return generateKeyPair(alg); + } + + // Assume file is a private key + const privateKey = createPrivateKey(file as unknown as string); + // Derive a public key from the private key + const publicKey = createPublicKey(privateKey); + return { privateKey, publicKey }; +} + const tokenConfig = config.get('auth.token'); -export const issueToken: IssueGrantTokenFunction = async ( - client: Client, +// TODO: Support key rotation and stuff +const { publicKey, privateKey } = await getKeyPair( + await tokenConfig.key, + tokenConfig.alg, +); +export const jwksPublic = { + keys: [ + { + kid, + alg: tokenConfig.alg, + use: 'sig', + ...(await exportJWK(publicKey)), + }, + ], +}; +const JWKS = createLocalJWKSet(jwksPublic); + +export interface TokenClaims extends JWTPayload { + scope: readonly string[]; + user: { id: string }; +} + +export async function getToken(issuer: string, claims: TokenClaims) { + try { + const jwt = new SignJWT(claims) + .setProtectedHeader({ + alg: tokenConfig.alg, + kid, + jku: config.get('auth.endpoints.certs'), + }) + .setJti(randomBytes(16).toString('hex')) + .setIssuedAt() + .setIssuer(issuer) + // ???: Should the audience be something different? + // .setAudience(issuer) + .setNotBefore(new Date()) + .setSubject(claims.user.id); + + if (tokenConfig.expiresIn) { + jwt.setExpirationTime(tokenConfig.expiresIn); + } + + return await jwt.sign(privateKey); + } catch (error: unknown) { + throw new Error('Failed to issue token', { cause: error }); + } +} + +export async function verifyToken(issuer: string, token: string) { + const { payload } = await jwtVerify(token, JWKS, { + issuer, + audience: issuer, + }); + return payload; +} + +export const issueToken = (async ( + _client: Client, user: DBUser, request: OAuth2Req, done, ) => { try { - const { scope } = request; - const token = await saveToken({ - token: makeHash(tokenConfig.length), - expiresIn: tokenConfig.expiresIn, - scope, + // TODO: Fill out user info + const token = await getToken(request.issuer, { user, - clientId: client.client_id, + scope: request.scope, }); // eslint-disable-next-line unicorn/no-null - done(null, token.token, { expires_in: token.expiresIn }); + done(null, token, { expires_in: tokenConfig.expiresIn }); } catch (error: unknown) { done(error as Error); } -}; +}) satisfies IssueGrantTokenFunction; -interface CodePayload extends OAuth2Req { +interface CodePayload { + issuer: string; user: User['id']; + scope: readonly string[]; } -const alg = 'HS256'; const authCode = config.get('auth.code'); -const key = (await authCode.key) ?? (await generateSecret(alg)); +const key = (await authCode.key) ?? (await generateSecret(authCode.alg)); export const issueCode: IssueGrantCodeFunctionArity6 = async ( client: Client, redirectUri, @@ -168,9 +241,10 @@ export const issueCode: IssueGrantCodeFunctionArity6 = async ( } const payload = { - ...request, user: user.id, - } satisfies CodePayload; + issuer: request.issuer, + scope: request.scope, + } as const satisfies CodePayload; const code = await new EncryptJWT(payload) .setProtectedHeader({ alg: 'dir', enc: 'A128CBC-HS256' }) .setSubject(redirectUri) @@ -202,8 +276,14 @@ const plugin: FastifyPluginAsync = async ( // PKCE oauth2server.grant(extensions()); - // Implicit flow (token) - oauth2server.grant(oauth2orize.grant.token(issueToken)); + oauth2server.grant('*', (request) => ({ + issuer: `${request.protocol}://${request.hostname}/` as const, + })); + + if (config.get('auth.oauth2.allowImplicitFlows')) { + // Implicit flow (token) + oauth2server.grant(oauth2orize.grant.token(issueToken)); + } // Code flow (code) oauth2server.grant(oauth2orize.grant.code(issueCode)); @@ -270,16 +350,13 @@ const plugin: FastifyPluginAsync = async ( } } - const { scope, user } = payload; - const { expiresIn, token } = await saveToken({ - token: makeHash(tokenConfig.length), - expiresIn: tokenConfig.expiresIn, - scope, + const { issuer, user, scope } = payload; + const token = await getToken(issuer, { user: { id: user! }, - clientId: client.client_id, + scope, }); const extras: Record = { - expires_in: expiresIn, + expires_in: tokenConfig.expiresIn, }; /** @@ -393,7 +470,6 @@ const plugin: FastifyPluginAsync = async ( const doDecision = promisifyMiddleware( oauth2server.decision((request, done) => { try { - // @ts-expect-error body const { scope, allow } = request.body as { scope?: string[]; allow?: unknown; diff --git a/oada/services/auth/src/oidc.ts b/oada/services/auth/src/oidc.ts index 76f0dbdb..3f26e753 100644 --- a/oada/services/auth/src/oidc.ts +++ b/oada/services/auth/src/oidc.ts @@ -19,9 +19,6 @@ import { join } from 'node:path/posix'; import { config } from './config.js'; -import { createPrivateKey, createPublicKey } from 'node:crypto'; -import type { File } from 'node:buffer'; - import type { FastifyPluginAsync } from 'fastify'; import fastifyAccepts from '@fastify/accepts'; @@ -31,7 +28,7 @@ import { type StrategyVerifyCallbackUserInfo, } from 'openid-client'; import { type OAuth2Server, createServer } from 'oauth2orize'; -import { SignJWT, exportJWK, generateKeyPair } from 'jose'; +import { SignJWT, exportJWK } from 'jose'; import oauth2orizeOpenId, { type IssueIDToken } from 'oauth2orize-openid'; import memoize from 'p-memoize'; @@ -45,7 +42,12 @@ import { register, update, } from './db/models/user.js'; -import { issueCode, issueToken } from './oauth2.js'; +import { + getKeyPair, + issueCode, + issueToken, + jwksPublic as oauthJWKs, +} from './oauth2.js'; import type { DBClient } from './db/models/client.js'; import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; import { createUserinfo } from './utils.js'; @@ -66,7 +68,7 @@ declare module 'oauth2orize' { } declare module 'openid-client' { - export interface TypeOfGenericClient { + interface TypeOfGenericClient { register( metadata: Omit, other?: RegisterOther & ClientOptions, @@ -76,25 +78,23 @@ declare module 'openid-client' { const idToken = config.get('auth.idToken'); -const kid = '1'; -const alg = 'PS256'; -// eslint-disable-next-line @typescript-eslint/ban-types -async function getKeyPair(file: File | null) { - if (!file) { - return generateKeyPair(alg); - } - - // Assume file is a private key - const privateKey = createPrivateKey(file as unknown as string); - // Derive a public key from the private key - const publicKey = createPublicKey(privateKey); - return { privateKey, publicKey }; -} +const kid = 'openid-1'; // TODO: Support key rotation and stuff -const { publicKey, privateKey } = await getKeyPair(await idToken.key); +const { publicKey, privateKey } = await getKeyPair( + await idToken.key, + idToken.alg, +); const jwksPublic = { - keys: [{ kid, ...(await exportJWK(publicKey)) }], + keys: [ + { + kid, + alg: idToken.alg, + use: 'sig', + ...(await exportJWK(publicKey)), + }, + ...oauthJWKs.keys, + ], }; const jwksPrivate = { keys: [{ kid, ...(await exportJWK(privateKey)) }], @@ -118,7 +118,7 @@ export const issueIdToken: IssueIDToken = async ( }; const token = await new SignJWT(payload) - .setProtectedHeader({ kid, alg }) + .setProtectedHeader({ kid, alg: idToken.alg }) .setIssuedAt() .setExpirationTime(idToken.expiresIn) .setAudience(client.client_id) @@ -143,20 +143,22 @@ const plugin: FastifyPluginAsync = async ( oauth2server.grant(oauth2orizeOpenId.extensions()); - // Implicit flow (id_token) - oauth2server.grant( - oauth2orizeOpenId.grant.idToken( - (client: DBClient, user: DBUser, ares, done) => { - ares.userinfo = true; - issueIdToken(client, user, ares, done); - }, - ), - ); + if (config.get('auth.oauth2.allowImplicitFlows')) { + // Implicit flow (id_token) + oauth2server.grant( + oauth2orizeOpenId.grant.idToken( + (client: DBClient, user: DBUser, ares, done) => { + ares.userinfo = true; + issueIdToken(client, user, ares, done); + }, + ), + ); - // Implicit flow (id_token token) - oauth2server.grant( - oauth2orizeOpenId.grant.idTokenToken(issueToken, issueIdToken), - ); + // Implicit flow (id_token token) + oauth2server.grant( + oauth2orizeOpenId.grant.idTokenToken(issueToken, issueIdToken), + ); + } // Hybrid flow (code id_token) oauth2server.grant( @@ -370,24 +372,37 @@ const plugin: FastifyPluginAsync = async ( userinfo_endpoint: `.${join('/', fastify.prefix, config.get('auth.endpoints.userinfo'))}`, jwks_uri: `.${join('/', fastify.prefix, config.get('auth.endpoints.certs'))}`, response_types_supported: [ + ...(config.get('auth.oauth2.allowImplicitFlows') + ? (['token', 'id_token', 'id_token token'] as const) + : []), 'code', - 'token', - 'id_token', 'code token', 'code id_token', - 'id_token token', 'code id_token token', ], subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256'], + id_token_signing_alg_values_supported: [config.get('auth.idToken.alg')], token_endpoint_auth_methods_supported: ['client_secret_post'], } as const; fastify.log.debug({ configuration }, 'Loaded OIDC configuration'); + // Redirect other OIDC config endpoints to openid-configuration endpoint + await fastify.register( + async (app) => { + app.all('/oada-configuration', async (_request, reply) => + reply.redirect(301, 'openid-configuration'), + ); + app.all('/oauth-authorization-server', async (_request, reply) => + reply.redirect(301, 'openid-configuration'), + ); + }, + { prefix: '/.well-known/' }, + ); + await fastify.register(wkj, { resources: { - 'oada-configuration': configuration, + 'openid-configuration': configuration, }, }); }; diff --git a/oada/services/http-handler/package.json b/oada/services/http-handler/package.json index 6be6c309..1375d8fb 100644 --- a/oada/services/http-handler/package.json +++ b/oada/services/http-handler/package.json @@ -43,7 +43,7 @@ "@fastify/rate-limit": "^9.1.0", "@fastify/request-context": "^5.1.0", "@fastify/sensible": "^5.5.0", - "@fastify/websocket": "^8.3.1", + "@fastify/websocket": "^9.0.0", "@oada/error": "^2.0.1", "@oada/formats-server": "^3.5.3", "@oada/lib-arangodb": "^3.7.0", @@ -71,6 +71,8 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@fastify/jwt": "^8.0.0", + "@oada/auth": "workspace:^", "@oada/users": "workspace:services/users", "@oada/write-handler": "^3.7.0", "@types/cacache": "^17.0.2", diff --git a/oada/services/http-handler/src/authorizations.ts b/oada/services/http-handler/src/authorizations.ts index d8900db4..5d2fbba6 100644 --- a/oada/services/http-handler/src/authorizations.ts +++ b/oada/services/http-handler/src/authorizations.ts @@ -57,7 +57,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { // Authorizations routes // TODO: How the heck should this work?? fastify.get('/', async (request, reply) => { - const results = await authorizations.findByUser(request.user.user_id); + const results = await authorizations.findByUser(request.user.id); // eslint-disable-next-line @typescript-eslint/ban-types const response: Record = {}; @@ -71,7 +71,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { fastify.get('/:authId', async (request, reply) => { const { authId } = request.params as { authId: string }; - const { user_id: userid } = request.user; + const { id: userid } = request.user; const auth = await authorizations.findById(authId); // Only let users see their own authorizations @@ -111,7 +111,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { const auth = { // TODO: Which fields should be selectable by the client? user: { - _id: request.user.user_id, + _id: request.user.id, }, clientId: request.user.client_id, createTime: Date.now(), @@ -122,7 +122,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { }; // Don't allow making tokens for other users unless admin.user - if (auth.user._id !== request.user.user_id) { + if (auth.user._id !== request.user.id) { if ( !request.user.scope.some( (s) => s === 'oada.admin.user:all' || 'oada.admin.user:write', @@ -160,7 +160,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { const auth = await authorizations.findById(authId); // Only let users see their own authorizations - if (auth?.user._id !== request.user.user_id) { + if (auth?.user._id !== request.user.id) { void reply.forbidden(); return; } diff --git a/oada/services/http-handler/src/resources.ts b/oada/services/http-handler/src/resources.ts index 3a27551e..2fe61bef 100644 --- a/oada/services/http-handler/src/resources.ts +++ b/oada/services/http-handler/src/resources.ts @@ -168,7 +168,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { const result = await resources.lookupFromUrl( `/${fullpath}`, - request.user.user_id, + request.user.id, ); request.log.trace({ result }, 'Graph lookup result'); if (result.resource_id) { @@ -195,7 +195,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { // Connection_id: request.id, // domain: request.headers.host, oadaGraph: request.oadaGraph, - user_id: request.user.user_id, + user_id: request.user.id, scope: request.user.scope as Scope[], contentType: request.headers['content-type'], // RequestType: request.method.toLowerCase(), @@ -212,7 +212,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { } else if (!response.permissions.owner && !response.permissions.write) { request.log.warn( '%s tried to PUT resource without proper permissions', - request.user.user_id, + request.user.id, ); void reply.forbidden( 'User does not have write permission for this resource', @@ -232,7 +232,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { if (!response.permissions.owner && !response.permissions.read) { request.log.warn( '%s tried to GET resource without proper permissions', - request.user.user_id, + request.user.id, ); void reply.forbidden( 'User does not have read permission for this resource', @@ -568,7 +568,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { 'resource_id': request.oadaGraph.resource_id, 'path_leftover': request.oadaGraph.path_leftover, // 'meta_id': oadaGraph['meta_id'], - 'user_id': request.user.user_id, + 'user_id': request.user.id, 'authorizationid': request.user.authorizationid, 'client_id': request.user.client_id, 'contentType': request.headers['content-type'], @@ -680,7 +680,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { 'resource_id': oadaGraph.resource_id, 'path_leftover': oadaGraph.path_leftover, // 'meta_id': oadaGraph['meta_id'], - 'user_id': request.user.user_id, + 'user_id': request.user.id, 'authorizationid': request.user.authorizationid, 'client_id': request.user.client_id, 'if-match': ifMatch, diff --git a/oada/services/http-handler/src/server.ts b/oada/services/http-handler/src/server.ts index 6811c775..ad9c17de 100644 --- a/oada/services/http-handler/src/server.ts +++ b/oada/services/http-handler/src/server.ts @@ -24,11 +24,13 @@ import { nstats } from '@oada/lib-prom'; import { plugin as formats } from '@oada/formats-server'; +import type { TokenClaims } from '@oada/auth'; import authorizations from './authorizations.js'; import resources from './resources.js'; import users from './users.js'; import websockets from './websockets.js'; +import type {} from '@fastify/jwt'; import { type Authenticate, fastifyJwtJwks } from 'fastify-jwt-jwks'; import { fastify as Fastify, @@ -141,11 +143,12 @@ export const fastify = Fastify({ if (process.env.NODE_ENV !== 'production') { // Send errors on to client for debug purposes - fastify.setErrorHandler((error, _request, reply) => { + fastify.setErrorHandler((error, request, reply) => { // @ts-expect-error stuff const res = error.response; - fastify.log.error({ err: error, res }); - void reply.code(500).send(res?.body ?? res); + const code = error.statusCode ?? 500; + request.log.error({ err: error, res }); + void reply.code(code).send(res?.body ?? res); }); // Add request id header for debugging purposes @@ -167,6 +170,7 @@ export async function start(): Promise { declare module '@fastify/request-context' { interface RequestContextData { id: string; + issuer: string; } } @@ -177,14 +181,14 @@ declare module 'fastify' { // eslint-disable-next-line @typescript-eslint/no-shadow interface FastifyRequest { user: { - expired: boolean; - authorizationid: string; - user_id: string; - user_scope: readonly string[]; - scope: readonly string[]; - bookmarks_id: string; - shares_id: string; - client_id: string; + readonly expired: boolean; + readonly authorizationid: string; + readonly id: string; + readonly user_scope: readonly string[]; + readonly scope: readonly string[]; + readonly bookmarks_id: string; + readonly shares_id: string; + readonly client_id: string; }; } } @@ -228,6 +232,10 @@ await fastify.register(fastifyRequestContext, { // Add id to request context fastify.addHook('onRequest', async (request) => { requestContext.set('id', request.id); + requestContext.set( + 'issuer', + `${request.protocol}://${request.hostname}/` as const, + ); }); await fastify.register(fastifySensible); @@ -320,11 +328,22 @@ const issuer = config.get('oidc.issuer'); const { jwks_uri: jwksUrl } = await discoverConfiguration(issuer); fastify.log.debug({ jwksUrl }, `Loaded OIDC configuration for ${issuer}`); -// eslint-disable-next-line @typescript-eslint/no-unsafe-argument +declare module '@fastify/jwt' { + interface FastifyJWT { + payload: TokenClaims; + user: { id: string }; + } +} + await fastify.register(fastifyJwtJwks, { jwksUrl, - formatUser(payload: unknown) { - return payload; + issuer: { + test(value: string) { + return requestContext.get('issuer') === value; + }, + }, + formatUser(claims) { + return claims.user; }, }); await fastify.register(async (instance) => { diff --git a/oada/services/http-handler/src/users.ts b/oada/services/http-handler/src/users.ts index bec53417..d6a7c2ca 100644 --- a/oada/services/http-handler/src/users.ts +++ b/oada/services/http-handler/src/users.ts @@ -186,7 +186,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) => { }); fastify.get('/me', async (request, reply) => { - await replyUser(request.user.user_id, reply); + await replyUser(request.user.id, reply); }); // TODO: don't return stuff to anyone anytime diff --git a/oada/services/http-handler/tsconfig.json b/oada/services/http-handler/tsconfig.json index 7c7d38f3..7759f68e 100644 --- a/oada/services/http-handler/tsconfig.json +++ b/oada/services/http-handler/tsconfig.json @@ -12,6 +12,7 @@ { "path": "../../libs/lib-arangodb" }, { "path": "../../libs/lib-prom" }, { "path": "../permissions-handler" }, + { "path": "../auth" }, { "path": "../write-handler" } ] } diff --git a/oada/services/permissions-handler/src/server.ts b/oada/services/permissions-handler/src/server.ts index 128056bf..aab1ef0b 100644 --- a/oada/services/permissions-handler/src/server.ts +++ b/oada/services/permissions-handler/src/server.ts @@ -150,7 +150,7 @@ export function handleRequest( // Let contentType = req.requestType === 'put' ? req.contentType : (resource ? resource._type : undefined); // trace('contentType = ', 'is put:', req.requestType === 'put', 'req.contentType:', req.contentType, 'resource:', resource); trace( - 'Does user have scope? resulting contentType: %s typeis check: %s', + 'Does user have scope? resulting contentType: %s typeIs check: %s', contentType, is, ); @@ -177,7 +177,7 @@ export function handleRequest( const write = scopePerm(perm, 'write'); trace('Does user have write scope? %s', write); trace('contentType is %s', contentType); - trace('write typeis %s %s', type, is); + trace('write typeIs %s %s', type, is); trace(scopes.get(type), 'scope types'); return is && write; }); diff --git a/oada/services/well-known/src/config.ts b/oada/services/well-known/src/config.ts index c1f8f5e0..5982f77d 100644 --- a/oada/services/well-known/src/config.ts +++ b/oada/services/well-known/src/config.ts @@ -76,7 +76,7 @@ export const { config, schema } = await libConfig({ env: 'WELLKNOWN_SUBSERVICES', arg: 'wellknown-subservices', }, - 'oada-configuration': { + 'openid-configuration': { format: Object, default: { well_known_version: '1.1.0', @@ -96,9 +96,9 @@ export const { config, schema } = await libConfig({ const server = config.get('wellKnown.server'); -if (!config.get('wellKnown.oada-configuration.oada_base_uri')) { +if (!config.get('wellKnown.openid-configuration.oada_base_uri')) { config.set( - 'wellKnown.server.oada-configuration.oada_base_uri', + 'wellKnown.server.openid-configuration.oada_base_uri', // eslint-disable-next-line sonarjs/no-nested-template-literals `${server.mode}//${server.domain}${server.port ? `:${server.port}` : ''}${ server.path_prefix ?? '' diff --git a/oada/services/well-known/src/index.ts b/oada/services/well-known/src/index.ts index 7e3a9bde..63b378fb 100644 --- a/oada/services/well-known/src/index.ts +++ b/oada/services/well-known/src/index.ts @@ -101,8 +101,8 @@ fastify.log.debug({ configuration }, `Loaded OIDC configuration for ${issuer}`); const wellKnownOptions = { resources: { - 'oada-configuration': { - ...config.get('wellKnown.oada-configuration'), + 'openid-configuration': { + ...config.get('wellKnown.openid-configuration'), ...configuration, }, }, @@ -114,14 +114,14 @@ const subservices = new Set( .map((s) => (typeof s === 'string' ? s : join(s.base, s.addPrefix ?? ''))), ); -// Redirect other OIDC config endpoints to oada-configuration endpoint +// Redirect other OIDC config endpoints to openid-configuration endpoint await fastify.register( async (app) => { - app.all('/openid-configuration', async (_request, reply) => - reply.redirect(301, './oada-configuration'), + app.all('/oada-configuration', async (_request, reply) => + reply.redirect(301, 'openid-configuration'), ); app.all('/oauth-authorization-server', async (_request, reply) => - reply.redirect(301, './oada-configuration'), + reply.redirect(301, 'openid-configuration'), ); }, { prefix: '/.well-known/' }, diff --git a/oada/yarn.lock b/oada/yarn.lock index da307bbb..1a1e4608 100644 --- a/oada/yarn.lock +++ b/oada/yarn.lock @@ -454,6 +454,19 @@ __metadata: languageName: node linkType: hard +"@fastify/jwt@npm:^8.0.0": + version: 8.0.0 + resolution: "@fastify/jwt@npm:8.0.0" + dependencies: + "@fastify/error": "npm:^3.0.0" + "@lukeed/ms": "npm:^2.0.0" + fast-jwt: "npm:^3.3.2" + fastify-plugin: "npm:^4.0.0" + steed: "npm:^1.1.3" + checksum: 10/e0953c2ca9c67619e237ed58366b03a4f846897d042b5d0ebb5b7d9a02777b1b4aec503635dea51fea94b13a4378033482cb91aef2915ed5b7b3a424d30180c3 + languageName: node + linkType: hard + "@fastify/passport@npm:^2.4.0": version: 2.4.0 resolution: "@fastify/passport@npm:2.4.0" @@ -559,23 +572,24 @@ __metadata: languageName: node linkType: hard -"@fastify/view@npm:^8.2.0": - version: 8.2.0 - resolution: "@fastify/view@npm:8.2.0" +"@fastify/view@npm:^9.0.0": + version: 9.0.0 + resolution: "@fastify/view@npm:9.0.0" dependencies: fastify-plugin: "npm:^4.0.0" hashlru: "npm:^2.3.0" - checksum: 10/8db89a1d56a5d60cbde99648aa8cc1ef3f9ea50a494a7e0674ed77dc43f87e18b1bb76b4fbf2cc523faf26dc5802a4a429bbef09e39d8b3294fdc26a204e2189 + checksum: 10/3a4ebb37e030d3360f787000ea0a86b0f1aaff276ae704adc5f4da86663ac7feddd531da8d5c7c7fa11a157928561632e319a28d569ce1fb226c61fd05ec97c0 languageName: node linkType: hard -"@fastify/websocket@npm:^8.3.1": - version: 8.3.1 - resolution: "@fastify/websocket@npm:8.3.1" +"@fastify/websocket@npm:^9.0.0": + version: 9.0.0 + resolution: "@fastify/websocket@npm:9.0.0" dependencies: + duplexify: "npm:^4.1.2" fastify-plugin: "npm:^4.0.0" ws: "npm:^8.0.0" - checksum: 10/e7088e357fcffc443dfd14929970247b0a882e4759f66ea181d1604698a783c3dcaf21b0546b66005497e89a17e9af7f5fcf11f479dcebf53f5f22dc358bc595 + checksum: 10/7920a6408629fd12151082e5e600ce8c8a125bba2c8ee499a2bd8a0a2d8541b149a491723ab295ac79033d9baace03bc4041d11503ac7dc8d72f61eba2718c7c languageName: node linkType: hard @@ -738,7 +752,7 @@ __metadata: languageName: node linkType: hard -"@oada/auth@workspace:services/auth": +"@oada/auth@workspace:^, @oada/auth@workspace:services/auth": version: 0.0.0-use.local resolution: "@oada/auth@workspace:services/auth" dependencies: @@ -746,6 +760,7 @@ __metadata: "@fastify/cors": "npm:^9.0.1" "@fastify/formbody": "npm:^7.4.0" "@fastify/helmet": "npm:^11.1.1" + "@fastify/jwt": "npm:^8.0.0" "@fastify/passport": "npm:^2.4.0" "@fastify/rate-limit": "npm:^9.1.0" "@fastify/request-context": "npm:^5.1.0" @@ -753,7 +768,7 @@ __metadata: "@fastify/sensible": "npm:^5.5.0" "@fastify/static": "npm:^7.0.1" "@fastify/type-provider-json-schema-to-ts": "npm:^3.0.0" - "@fastify/view": "npm:^8.2.0" + "@fastify/view": "npm:^9.0.0" "@oada/certs": "npm:^4.1.1" "@oada/error": "npm:^2.0.1" "@oada/lib-arangodb": "npm:^3.7.0" @@ -920,10 +935,12 @@ __metadata: "@fastify/bearer-auth": "npm:^9.3.0" "@fastify/cors": "npm:^9.0.1" "@fastify/helmet": "npm:^11.1.1" + "@fastify/jwt": "npm:^8.0.0" "@fastify/rate-limit": "npm:^9.1.0" "@fastify/request-context": "npm:^5.1.0" "@fastify/sensible": "npm:^5.5.0" - "@fastify/websocket": "npm:^8.3.1" + "@fastify/websocket": "npm:^9.0.0" + "@oada/auth": "workspace:^" "@oada/error": "npm:^2.0.1" "@oada/formats-server": "npm:^3.5.3" "@oada/lib-arangodb": "npm:^3.7.0" @@ -1043,7 +1060,7 @@ __metadata: convict-format-with-moment: "npm:^6.2.0" convict-format-with-validator: "npm:^6.2.0" debug: "npm:^4.3.4" - dotenv: "npm:^16.4.4" + dotenv: "npm:^16.4.5" json5: "npm:^2.2.3" tslib: "npm:2.6.2" yaml: "npm:^2.3.4" @@ -1172,7 +1189,7 @@ __metadata: "@types/node": "npm:^20.11.19" cls-rtracer: "npm:^2.6.3" is-interactive: "npm:^2.0.0" - pino: "npm:^8.18.0" + pino: "npm:^8.19.0" pino-caller: "npm:^3.4.0" pino-debug: "npm:^2.0.0" pino-loki: "npm:^2.2.1" @@ -1218,8 +1235,8 @@ __metadata: "@types/eslint": "npm:^8.56.2" "@types/mocha": "npm:^10.0.6" "@types/node": "npm:^20.11.19" - "@typescript-eslint/eslint-plugin": "npm:^7.0.1" - "@typescript-eslint/parser": "npm:^7.0.1" + "@typescript-eslint/eslint-plugin": "npm:^7.0.2" + "@typescript-eslint/parser": "npm:^7.0.2" "@yarnpkg/sdks": "npm:^3.1.0" browserslist: "npm:^4.23.0" c8: "npm:^9.1.0" @@ -2161,15 +2178,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/eslint-plugin@npm:7.0.1" +"@typescript-eslint/eslint-plugin@npm:^7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/eslint-plugin@npm:7.0.2" dependencies: "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:7.0.1" - "@typescript-eslint/type-utils": "npm:7.0.1" - "@typescript-eslint/utils": "npm:7.0.1" - "@typescript-eslint/visitor-keys": "npm:7.0.1" + "@typescript-eslint/scope-manager": "npm:7.0.2" + "@typescript-eslint/type-utils": "npm:7.0.2" + "@typescript-eslint/utils": "npm:7.0.2" + "@typescript-eslint/visitor-keys": "npm:7.0.2" debug: "npm:^4.3.4" graphemer: "npm:^1.4.0" ignore: "npm:^5.2.4" @@ -2182,7 +2199,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/0862e8ec8677fcea794394fc9eab8dba11043c08452722790e0d296d4ee84713180676e1e3135be4203ace7bb73933c94159255cb9190c7bc13bf7f03a361915 + checksum: 10/430b2f7ca36ee73dc75c1d677088709f3c9d5bbb4fffa3cfbe1b7d63979ee397f7a4a2a1386e05a04991500fa0ab0dd5272e8603a2b20f42e4bf590603500858 languageName: node linkType: hard @@ -2204,21 +2221,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/parser@npm:7.0.1" +"@typescript-eslint/parser@npm:^7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/parser@npm:7.0.2" dependencies: - "@typescript-eslint/scope-manager": "npm:7.0.1" - "@typescript-eslint/types": "npm:7.0.1" - "@typescript-eslint/typescript-estree": "npm:7.0.1" - "@typescript-eslint/visitor-keys": "npm:7.0.1" + "@typescript-eslint/scope-manager": "npm:7.0.2" + "@typescript-eslint/types": "npm:7.0.2" + "@typescript-eslint/typescript-estree": "npm:7.0.2" + "@typescript-eslint/visitor-keys": "npm:7.0.2" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/b4ba1743ab730268a1924139f072e4a0a56959526fb6377e1b3964518b6c6851733ae446a44d29fed1cb96669e2913cca524895ce77a6205aaed8bda00e8cd5d + checksum: 10/18d6e1bda64013f7d66164164c57a10390f7979db55b265062ae9337e11e0921bffca10870e252cd0bd198f79ffa2e87a652e57110e5b1b4cc738453154c205c languageName: node linkType: hard @@ -2232,13 +2249,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/scope-manager@npm:7.0.1" +"@typescript-eslint/scope-manager@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/scope-manager@npm:7.0.2" dependencies: - "@typescript-eslint/types": "npm:7.0.1" - "@typescript-eslint/visitor-keys": "npm:7.0.1" - checksum: 10/dade6055bb853adb54de795cc3da5ab8550236d4186f108573fdb02e636ab7fc4300a55b506698ced4087ca43b143a5593931cb3195ab4790470b456d9ff8846 + "@typescript-eslint/types": "npm:7.0.2" + "@typescript-eslint/visitor-keys": "npm:7.0.2" + checksum: 10/773ea6e61f741777e69a469641f3db0d3c2301c0102667825fb235ed5a65c95f6d6b31b19e734b9a215acc0c7c576c65497635b8d5928eeddb58653ceb13d2d5 languageName: node linkType: hard @@ -2259,12 +2276,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/type-utils@npm:7.0.1" +"@typescript-eslint/type-utils@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/type-utils@npm:7.0.2" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.0.1" - "@typescript-eslint/utils": "npm:7.0.1" + "@typescript-eslint/typescript-estree": "npm:7.0.2" + "@typescript-eslint/utils": "npm:7.0.2" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.0.1" peerDependencies: @@ -2272,7 +2289,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/cf20a3c0e56121ac62467e48121e135798db6d2999bd4f96ed44edc39f2597812d12b1bd6a378adec54d6c5e7db75fa5f98a27ce399792a2c8a5bbd3649952f7 + checksum: 10/63bf19c9f5bbcb0f3e127f509d85dc49be4e5e51781d78f58c96786089e7c909b25d35d0248a6a758e2f7d5b5223d2262c2d597ab71f226af6beb499ae950645 languageName: node linkType: hard @@ -2283,10 +2300,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/types@npm:7.0.1" - checksum: 10/c08b2d34bab2a877a45a1e4c2923f50d03022b682b7aaba929ae2a9a5ad32db0e46265544a6616ccb98654b434250621be0e282fc5b21b8ccaf6b78741d68f67 +"@typescript-eslint/types@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/types@npm:7.0.2" + checksum: 10/2cba8a0355cc7357db142fa597d02cf39e1d1cb0ec87c80e91daaa2b87f2a794d2649def9d7b2aa435691c3810d2cbd4cdc21668b19b991863f0d54d4a22da82 languageName: node linkType: hard @@ -2309,12 +2326,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/typescript-estree@npm:7.0.1" +"@typescript-eslint/typescript-estree@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/typescript-estree@npm:7.0.2" dependencies: - "@typescript-eslint/types": "npm:7.0.1" - "@typescript-eslint/visitor-keys": "npm:7.0.1" + "@typescript-eslint/types": "npm:7.0.2" + "@typescript-eslint/visitor-keys": "npm:7.0.2" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -2324,7 +2341,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/b0b0adc84502d1ffcf3a0024179e0f2780be5f8b0a18328db46d430efc4e38a7965656b4392dd47d6176bbb1ee200aec6dd8581c39b606e260750574358cde9f + checksum: 10/307080e29c22fc69f0ce7ab7101e1629e05f45a9e541c250e03d06b61336ab0ccb5f0a7354ee3da4e38d5cade4dd2fb7bb396cd7cbe74c2c4b3e29706a70abcc languageName: node linkType: hard @@ -2345,20 +2362,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/utils@npm:7.0.1" +"@typescript-eslint/utils@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/utils@npm:7.0.2" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" "@types/json-schema": "npm:^7.0.12" "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:7.0.1" - "@typescript-eslint/types": "npm:7.0.1" - "@typescript-eslint/typescript-estree": "npm:7.0.1" + "@typescript-eslint/scope-manager": "npm:7.0.2" + "@typescript-eslint/types": "npm:7.0.2" + "@typescript-eslint/typescript-estree": "npm:7.0.2" semver: "npm:^7.5.4" peerDependencies: eslint: ^8.56.0 - checksum: 10/b7e0cb2994f73b3f416684dc175d4e1da5f8306d6c81abbad2f219fa3e4f29154063a3c9568e4a1f879a38b79c62250e596e4ed7265f7bd1ed9b3db806cb92b7 + checksum: 10/e68bac777419cd529371f7f29f534efaeca130c90ed9723bfc7aac451d61ca3fc4ebd310e2c015e29e8dc7be4734ae46258ca8755897d7f5e3bb502660d5372f languageName: node linkType: hard @@ -2372,13 +2389,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.0.1": - version: 7.0.1 - resolution: "@typescript-eslint/visitor-keys@npm:7.0.1" +"@typescript-eslint/visitor-keys@npm:7.0.2": + version: 7.0.2 + resolution: "@typescript-eslint/visitor-keys@npm:7.0.2" dependencies: - "@typescript-eslint/types": "npm:7.0.1" + "@typescript-eslint/types": "npm:7.0.2" eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/915c5b19302a4c76e843cd2d04a9a2b11907e658d7018c8b55c338b090d9115d3719809aa05b8af130cc1b216c77626d210c20f705b732e83d04ceae0c112f6b + checksum: 10/da6c1b0729af99216cde3a65d4e91584a81fc6c9dff7ba291089f01bf7262de375f58c4c4246e5fbc29f51258db7725d9c830f82ccbd1cda812fd13c51480cda languageName: node linkType: hard @@ -4413,10 +4430,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.3.1, dotenv@npm:^16.4.4": - version: 16.4.4 - resolution: "dotenv@npm:16.4.4" - checksum: 10/ddf43ede209d5f54c9da688e93326162eb0fc367ad1869909568cb15571ab8c87a0fab4e1d4af9860be0571c707a8b4aefa060a4f1b0bb14298a81e0ccf0017d +"dotenv@npm:^16.3.1, dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 languageName: node linkType: hard @@ -4427,6 +4444,18 @@ __metadata: languageName: node linkType: hard +"duplexify@npm:^4.1.2": + version: 4.1.2 + resolution: "duplexify@npm:4.1.2" + dependencies: + end-of-stream: "npm:^1.4.1" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + stream-shift: "npm:^1.0.0" + checksum: 10/eeb4f362defa4da0b2474d853bc4edfa446faeb1bde76819a68035632c118de91f6a58e6fe05c84f6e6de2548f8323ec8473aa9fe37332c99e4d77539747193e + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -4512,7 +4541,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -9182,9 +9211,9 @@ __metadata: languageName: node linkType: hard -"pino@npm:^8.17.0, pino@npm:^8.18.0": - version: 8.18.0 - resolution: "pino@npm:8.18.0" +"pino@npm:^8.17.0, pino@npm:^8.19.0": + version: 8.19.0 + resolution: "pino@npm:8.19.0" dependencies: atomic-sleep: "npm:^1.0.0" fast-redact: "npm:^3.1.1" @@ -9199,7 +9228,7 @@ __metadata: thread-stream: "npm:^2.0.0" bin: pino: bin.js - checksum: 10/c9789a4e75bd5bb5d70561e429b7c23b2da1d42a69181814e93b2de53a3f2cdcb890f737927663fe92bab1e16e50aab1c7e62a00d4be4520fe7e8e9cbf1a838a + checksum: 10/c98e8bedb7c9eca5c0e75c2dd910a58b0e470da282c5a4787873a591666cc7cce33561d9ba6d6a20cf6bc4bc8d15b7db84cf6156f262081a5c6b8de134285789 languageName: node linkType: hard @@ -9501,7 +9530,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.1.1, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -10323,6 +10352,13 @@ __metadata: languageName: node linkType: hard +"stream-shift@npm:^1.0.0": + version: 1.0.3 + resolution: "stream-shift@npm:1.0.3" + checksum: 10/a24c0a3f66a8f9024bd1d579a533a53be283b4475d4e6b4b3211b964031447bdf6532dd1f3c2b0ad66752554391b7c62bd7ca4559193381f766534e723d50242 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3"