feat(authentication): Google authentication provider (#6104)

* init

* add entity_id and update provider

* initial implementation

* update lockfile

* fix conflicts

* add config variables

* update types

* refactor google provider

* re-order methods

* fix pr feedback p. 1

* add initial type and update callback authorization

* add google provider to integration test

* fix feedback

* initial implementation (#6171)

* initial implementation

* remove oauth lib

* move abstract authentication provider

* shuffle files around

* Update packages/authentication/src/migrations/Migration20240122041959.ts

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>

* call verify with token

---------

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Philip Korsholm
2024-01-24 09:57:58 +08:00
committed by GitHub
parent c37c82c5b5
commit b3d013940f
24 changed files with 402 additions and 87 deletions

View File

@@ -1,11 +1,10 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { MikroOrmWrapper } from "../../../utils"
import { initialize } from "../../../../src"
import { DB_URL } from "@medusajs/pricing/integration-tests/utils"
import { MedusaModule } from "@medusajs/modules-sdk"
import { IAuthenticationModuleService } from "@medusajs/types"
import { MedusaModule } from "@medusajs/modules-sdk"
import { MikroOrmWrapper } from "../../../utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { createAuthProviders } from "../../../__fixtures__/auth-provider"
import { initialize } from "../../../../src"
jest.setTimeout(30000)
@@ -24,7 +23,7 @@ describe("AuthenticationModuleService - AuthProvider", () => {
},
})
if(service.__hooks?.onApplicationStart) {
if (service.__hooks?.onApplicationStart) {
await service.__hooks.onApplicationStart()
}
})
@@ -39,12 +38,18 @@ describe("AuthenticationModuleService - AuthProvider", () => {
const authProviders = await service.listAuthProviders()
const serialized = JSON.parse(JSON.stringify(authProviders))
expect(serialized).toEqual([
expect.objectContaining({
provider: "usernamePassword",
name: "Username/Password Authentication",
}),
])
expect(serialized).toEqual(
expect.arrayContaining([
expect.objectContaining({
provider: "usernamePassword",
name: "Username/Password Authentication",
}),
expect.objectContaining({
provider: "google",
name: "Google Authentication",
}),
])
)
})
})

View File

@@ -1,13 +1,12 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import Scrypt from "scrypt-kdf"
import { MikroOrmWrapper } from "../../../utils"
import { initialize } from "../../../../src"
import { DB_URL } from "@medusajs/pricing/integration-tests/utils"
import { MedusaModule } from "@medusajs/modules-sdk"
import { IAuthenticationModuleService } from "@medusajs/types"
import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { MedusaModule } from "@medusajs/modules-sdk"
import { MikroOrmWrapper } from "../../../utils"
import Scrypt from "scrypt-kdf"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { createAuthProviders } from "../../../__fixtures__/auth-provider"
import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { initialize } from "../../../../src"
jest.setTimeout(30000)
const seedDefaultData = async (testManager) => {

View File

@@ -56,7 +56,9 @@
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"dotenv": "^16.1.4",
"jsonwebtoken": "^9.0.2",
"knex": "2.4.2",
"scrypt-kdf": "^2.0.1"
"scrypt-kdf": "^2.0.1",
"simple-oauth2": "^5.0.0"
}
}

View File

@@ -1,9 +1,14 @@
import * as defaultProviders from "@providers"
import {
AwilixContainer,
ClassOrFunctionReturning,
Constructor,
Resolver,
asClass,
} from "awilix"
import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types"
import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix"
export default async ({
container,
}: LoaderOptions<
@@ -18,7 +23,9 @@ export default async ({
for (const provider of providersToLoad) {
container.register({
[`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(),
[`auth_provider_${provider.PROVIDER}`]: asClass(
provider as Constructor<any>
).singleton(),
})
}

View File

@@ -39,6 +39,15 @@
],
"mappedType": "enum"
},
"config": {
"name": "config",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"is_active": {
"name": "is_active",
"type": "boolean",

View File

@@ -1,21 +0,0 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240104154451 extends Migration {
async up(): Promise<void> {
this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));');
this.addSql('create table "auth_user" ("id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));');
this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;');
}
async down(): Promise<void> {
this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";');
this.addSql('drop table if exists "auth_provider" cascade;');
this.addSql('drop table if exists "auth_user" cascade;');
}
}

View File

@@ -1,15 +0,0 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240115092929 extends Migration {
async up(): Promise<void> {
this.addSql('alter table "auth_user" add column "entity_id" text not null;');
this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");');
}
async down(): Promise<void> {
this.addSql('alter table "auth_user" drop constraint "IDX_auth_user_provider_entity_id";');
this.addSql('alter table "auth_user" drop column "entity_id";');
}
}

View File

@@ -0,0 +1,22 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240122041959 extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));');
this.addSql('create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));');
this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");');
this.addSql('alter table "auth_user" add constraint if not exists "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;');
}
async down(): Promise<void> {
this.addSql('alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";');
this.addSql('drop table if exists "auth_provider" cascade;');
this.addSql('drop table if exists "auth_user" cascade;');
}
}

View File

@@ -5,9 +5,10 @@ import {
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { ProviderDomain } from "../types/repositories/auth-provider"
type OptionalFields = "domain" | "is_active"
type OptionalFields = "domain" | "is_active" | "config"
@Entity()
export default class AuthProvider {
@@ -22,6 +23,9 @@ export default class AuthProvider {
@Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL })
domain: ProviderDomain = ProviderDomain.ALL
@Property({ columnType: "jsonb", nullable: true })
config: Record<string, unknown> | null = null
@Property({ columnType: "boolean", default: false })
is_active = false
}

View File

@@ -0,0 +1,209 @@
import {
AbstractAuthenticationModuleProvider,
MedusaError,
} from "@medusajs/utils"
import { AuthProviderService, AuthUserService } from "@services"
import jwt, { JwtPayload } from "jsonwebtoken"
import { AuthProvider } from "@models"
import { AuthenticationResponse } from "@medusajs/types"
import { AuthorizationCode } from "simple-oauth2"
import url from "url"
type InjectedDependencies = {
authUserService: AuthUserService
authProviderService: AuthProviderService
}
type AuthenticationInput = {
connection: { encrypted: boolean }
url: string
headers: { host: string }
query: Record<string, string>
body: Record<string, string>
}
type ProviderConfig = {
clientID: string
clientSecret: string
callbackURL: string
}
class GoogleProvider extends AbstractAuthenticationModuleProvider {
public static PROVIDER = "google"
public static DISPLAY_NAME = "Google Authentication"
protected readonly authUserSerivce_: AuthUserService
protected readonly authProviderService_: AuthProviderService
constructor({ authUserService, authProviderService }: InjectedDependencies) {
super()
this.authUserSerivce_ = authUserService
this.authProviderService_ = authProviderService
}
private async validateConfig(config: Partial<ProviderConfig>) {
if (!config.clientID) {
throw new Error("Google clientID is required")
}
if (!config.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!config.callbackURL) {
throw new Error("Google callbackUrl is required")
}
}
private originalURL(req: AuthenticationInput) {
const tls = req.connection.encrypted,
host = req.headers.host,
protocol = tls ? "https" : "http",
path = req.url || ""
return protocol + "://" + host + path
}
async getProviderConfig(req: AuthenticationInput): Promise<ProviderConfig> {
const { config } = (await this.authProviderService_.retrieve(
GoogleProvider.PROVIDER
)) as AuthProvider & { config: ProviderConfig }
this.validateConfig(config || {})
const { callbackURL } = config
const parsedCallbackUrl = !url.parse(callbackURL).protocol
? url.resolve(this.originalURL(req), callbackURL)
: callbackURL
return { ...config, callbackURL: parsedCallbackUrl }
}
async authenticate(
req: AuthenticationInput
): Promise<AuthenticationResponse> {
if (req.query && req.query.error) {
return {
success: false,
error: `${req.query.error_description}, read more at: ${req.query.error_uri}`,
}
}
let config
try {
config = await this.getProviderConfig(req)
} catch (error) {
return { success: false, error: error.message }
}
let { callbackURL, clientID, clientSecret } = config
const meta: ProviderConfig = {
clientID,
callbackURL,
clientSecret,
}
const code = (req.query && req.query.code) || (req.body && req.body.code)
// Redirect to google
if (!code) {
return this.getRedirect(meta)
}
return await this.validateCallback(code, meta)
}
// abstractable
private async validateCallback(
code: string,
{ clientID, callbackURL, clientSecret }: ProviderConfig
) {
const client = this.getAuthorizationCodeHandler({ clientID, clientSecret })
const tokenParams = {
code,
redirect_uri: callbackURL,
}
try {
const accessToken = await client.getToken(tokenParams)
return await this.verify_(accessToken.token.id_token)
} catch (error) {
return { success: false, error: error.message }
}
}
// abstractable
async verify_(refreshToken: string) {
const jwtData = (await jwt.decode(refreshToken, {
complete: true,
})) as JwtPayload
const entity_id = jwtData.payload.email
let authUser
try {
authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
entity_id,
GoogleProvider.PROVIDER
)
} catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) {
authUser = await this.authUserSerivce_.create([
{
entity_id,
provider_id: GoogleProvider.PROVIDER,
user_metadata: jwtData!.payload,
},
])
} else {
return { success: false, error: error.message }
}
}
return { success: true, authUser }
}
// Abstractable
private getRedirect({ clientID, callbackURL, clientSecret }: ProviderConfig) {
const client = this.getAuthorizationCodeHandler({ clientID, clientSecret })
const location = client.authorizeURL({
redirect_uri: callbackURL,
scope: "email profile",
})
return { success: true, location }
}
private getAuthorizationCodeHandler({
clientID,
clientSecret,
}: {
clientID: string
clientSecret: string
}) {
const config = {
client: {
id: clientID,
secret: clientSecret,
},
auth: {
// TODO: abstract to not be google specific
authorizeHost: "https://accounts.google.com",
authorizePath: "/o/oauth2/v2/auth",
tokenHost: "https://www.googleapis.com",
tokenPath: "/oauth2/v4/token",
},
}
return new AuthorizationCode(config)
}
}
export default GoogleProvider

View File

@@ -1 +1,2 @@
export { default as UsernamePasswordProvider } from "./username-password"
export { default as GoogleProvider } from "./google"

View File

@@ -1,11 +1,8 @@
import {
AbstractAuthenticationModuleProvider,
AuthenticationResponse,
} from "@medusajs/types"
import { AuthenticationResponse } from "@medusajs/types"
import { AuthUserService } from "@services"
import Scrypt from "scrypt-kdf"
import { isString } from "@medusajs/utils"
import { AbstractAuthenticationModuleProvider, isString } from "@medusajs/utils"
class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider {
public static PROVIDER = "usernamePassword"

View File

@@ -1,5 +1,4 @@
import {
AbstractAuthenticationModuleProvider,
AuthenticationResponse,
AuthenticationTypes,
Context,
@@ -16,6 +15,7 @@ import { joinerConfig } from "../joiner-config"
import { AuthProviderService, AuthUserService } from "@services"
import {
AbstractAuthenticationModuleProvider,
InjectManager,
InjectTransactionManager,
MedusaContext,
@@ -158,7 +158,7 @@ export default class AuthenticationModuleService<
protected async createAuthProviders_(
data: any[],
@MedusaContext() sharedContext: Context
): Promise<AuthenticationTypes.AuthProviderDTO[]> {
): Promise<TAuthProvider[]> {
return await this.authProviderService_.create(data, sharedContext)
}
@@ -196,7 +196,7 @@ export default class AuthenticationModuleService<
async updateAuthProvider_(
data: AuthenticationTypes.UpdateAuthProviderDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<AuthProviderDTO[]> {
): Promise<TAuthProvider[]> {
return await this.authProviderService_.update(data, sharedContext)
}
@@ -380,15 +380,14 @@ export default class AuthenticationModuleService<
await this.retrieveAuthProvider(provider, {})
registeredProvider = this.getRegisteredAuthenticationProvider(provider)
return await registeredProvider.authenticate(authenticationData)
} catch (error) {
return { success: false, error: error.message }
}
}
private async createProvidersOnLoad() {
private async createProvidersOnLoad() {
const providersToLoad = this.__container__["auth_providers"]
const providers = await this.authProviderService_.list({

View File

@@ -5,6 +5,7 @@ export type CreateAuthProviderDTO = {
name: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
export type UpdateAuthProviderDTO = {
@@ -13,6 +14,7 @@ export type UpdateAuthProviderDTO = {
name?: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
provider: AuthProvider
}

View File

@@ -3,6 +3,7 @@ export type AuthProviderDTO = {
name: string
domain: ProviderDomain
is_active: boolean
config: Record<string, unknown>
}
export type CreateAuthProviderDTO = {
@@ -10,6 +11,7 @@ export type CreateAuthProviderDTO = {
name: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
export type UpdateAuthProviderDTO = {
@@ -17,6 +19,7 @@ export type UpdateAuthProviderDTO = {
name?: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
export enum ProviderDomain {

View File

@@ -5,6 +5,7 @@ export type AuthProviderDTO = {
name: string
domain: ProviderDomain
is_active: boolean
config: Record<string, unknown> | null
}
export type CreateAuthProviderDTO = {
@@ -12,6 +13,7 @@ export type CreateAuthProviderDTO = {
name: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
export type UpdateAuthProviderDTO = {
@@ -19,6 +21,7 @@ export type UpdateAuthProviderDTO = {
name?: string
domain?: ProviderDomain
is_active?: boolean
config?: Record<string, unknown>
}
export enum ProviderDomain {

View File

@@ -1,2 +1,3 @@
export * from "./auth-user"
export * from "./auth-provider"
export * from "./provider"

View File

@@ -0,0 +1,5 @@
export type AuthenticationResponse = {
success: boolean
authUser?: any
error?: string
}

View File

@@ -1,3 +1,2 @@
export * from "./service"
export * from "./common"
export * from "./provider"

View File

@@ -1,5 +1,5 @@
import { IModuleService } from "../modules-sdk"
import {
AuthenticationResponse,
AuthProviderDTO,
AuthUserDTO,
CreateAuthProviderDTO,
@@ -9,9 +9,10 @@ import {
UpdateAuthProviderDTO,
UpdateAuthUserDTO,
} from "./common"
import { FindConfig } from "../common"
import { Context } from "../shared-context"
import { AuthenticationResponse } from "./provider"
import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
export interface IAuthenticationModuleService extends IModuleService {
authenticate(

View File

@@ -1,24 +1,19 @@
import { AuthUserDTO } from "./common"
import { AuthenticationResponse } from "@medusajs/types";
export abstract class AbstractAuthenticationModuleProvider {
public static PROVIDER: string
public static DISPLAY_NAME: string
public get provider() {
return (this.constructor as Function & { PROVIDER: string}).PROVIDER
return (this.constructor as Function & { PROVIDER: string }).PROVIDER
}
public get displayName() {
return (this.constructor as Function & { DISPLAY_NAME: string}).DISPLAY_NAME
return (this.constructor as Function & { DISPLAY_NAME: string })
.DISPLAY_NAME
}
abstract authenticate(
data: Record<string, unknown>
): Promise<AuthenticationResponse>
}
export type AuthenticationResponse = {
success: boolean
authUser?: AuthUserDTO
error?: string
}

View File

@@ -0,0 +1 @@
export * from "./abstract-authentication-provider"

View File

@@ -1,3 +1,4 @@
export * from "./authentication"
export * from "./bundles"
export * from "./common"
export * from "./dal"

View File

@@ -6060,14 +6060,44 @@ __metadata:
languageName: node
linkType: hard
"@hapi/hoek@npm:^9.0.0":
"@hapi/boom@npm:^10.0.1":
version: 10.0.1
resolution: "@hapi/boom@npm:10.0.1"
dependencies:
"@hapi/hoek": ^11.0.2
checksum: e4ae8a69bb67c5687320d320a0706ac66e797a659c19fb1c9b909eaefe3b41780e4ecd4382de1297b10c33e9db81f79667324576b9153f57b0cf701293b908d0
languageName: node
linkType: hard
"@hapi/bourne@npm:^3.0.0":
version: 3.0.0
resolution: "@hapi/bourne@npm:3.0.0"
checksum: 2e2df62f6bc6f32b980ba5bbdc09200c93c55c8306399ec0f2781da088a82aab699498c89fe94fec4acf770210f9aee28c75bfc2f04044849ac01b034134e717
languageName: node
linkType: hard
"@hapi/hoek@npm:^10.0.1":
version: 10.0.1
resolution: "@hapi/hoek@npm:10.0.1"
checksum: 320d5dc7a4070fa29e6344a3af9e44854980c6606848f7b7f59715174880cc09a1fe1e8adf44cf887100bd8d6a8664e9dc415986b30dc91df13455f7114de549
languageName: node
linkType: hard
"@hapi/hoek@npm:^11.0.2":
version: 11.0.4
resolution: "@hapi/hoek@npm:11.0.4"
checksum: 3c0e487824daaf3af4c29e46fd57b0c5801ce9164fef2417c70e271cd970e13cc542b196f70ba1cfc9ef944eed825fcac261085ab5e2928c6017428bf576b363
languageName: node
linkType: hard
"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0":
version: 9.3.0
resolution: "@hapi/hoek@npm:9.3.0"
checksum: a096063805051fb8bba4c947e293c664b05a32b47e13bc654c0dd43813a1cec993bdd8f29ceb838020299e1d0f89f68dc0d62a603c13c9cc8541963f0beca055
languageName: node
linkType: hard
"@hapi/topo@npm:^5.0.0":
"@hapi/topo@npm:^5.0.0, @hapi/topo@npm:^5.1.0":
version: 5.1.0
resolution: "@hapi/topo@npm:5.1.0"
dependencies:
@@ -6076,6 +6106,17 @@ __metadata:
languageName: node
linkType: hard
"@hapi/wreck@npm:^18.0.0":
version: 18.0.1
resolution: "@hapi/wreck@npm:18.0.1"
dependencies:
"@hapi/boom": ^10.0.1
"@hapi/bourne": ^3.0.0
"@hapi/hoek": ^11.0.2
checksum: 46b1b1f750a66c4724964eb6d9192d1d19cfa45e602386aae76f52e3b423c9ae14a03a0f0e9f962e7d973708e1b0b6ab42d2ae77539a691fa77a18c78ccf285c
languageName: node
linkType: hard
"@headlessui/react@npm:^1.7.18":
version: 1.7.18
resolution: "@headlessui/react@npm:1.7.18"
@@ -7854,10 +7895,12 @@ __metadata:
cross-env: ^5.2.1
dotenv: ^16.1.4
jest: ^29.6.3
jsonwebtoken: ^9.0.2
knex: 2.4.2
medusa-test-utils: ^1.1.40
rimraf: ^3.0.2
scrypt-kdf: ^2.0.1
simple-oauth2: ^5.0.0
ts-jest: ^29.1.1
ts-node: ^10.9.1
tsc-alias: ^1.8.6
@@ -12192,7 +12235,7 @@ __metadata:
languageName: node
linkType: hard
"@sideway/address@npm:^4.1.3":
"@sideway/address@npm:^4.1.3, @sideway/address@npm:^4.1.4":
version: 4.1.4
resolution: "@sideway/address@npm:4.1.4"
dependencies:
@@ -35009,6 +35052,19 @@ __metadata:
languageName: node
linkType: hard
"joi@npm:^17.6.4":
version: 17.12.0
resolution: "joi@npm:17.12.0"
dependencies:
"@hapi/hoek": ^9.3.0
"@hapi/topo": ^5.1.0
"@sideway/address": ^4.1.4
"@sideway/formula": ^3.0.1
"@sideway/pinpoint": ^2.0.0
checksum: 2378f4ec8de2bc12674ce3e6faac509f52ff4f734c67bf68c288816b20336d4e59433ea1c1e187f1009075c81ec5fa8b5061094feb37a855d6e3ee0cfcd79dd8
languageName: node
linkType: hard
"join-component@npm:^1.1.0":
version: 1.1.0
resolution: "join-component@npm:1.1.0"
@@ -35478,6 +35534,24 @@ __metadata:
languageName: node
linkType: hard
"jsonwebtoken@npm:^9.0.2":
version: 9.0.2
resolution: "jsonwebtoken@npm:9.0.2"
dependencies:
jws: ^3.2.2
lodash.includes: ^4.3.0
lodash.isboolean: ^3.0.3
lodash.isinteger: ^4.0.4
lodash.isnumber: ^3.0.3
lodash.isplainobject: ^4.0.6
lodash.isstring: ^4.0.1
lodash.once: ^4.0.0
ms: ^2.1.1
semver: ^7.5.4
checksum: d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131
languageName: node
linkType: hard
"jsprim@npm:^1.2.2":
version: 1.4.2
resolution: "jsprim@npm:1.4.2"
@@ -46081,6 +46155,18 @@ __metadata:
languageName: node
linkType: hard
"simple-oauth2@npm:^5.0.0":
version: 5.0.0
resolution: "simple-oauth2@npm:5.0.0"
dependencies:
"@hapi/hoek": ^10.0.1
"@hapi/wreck": ^18.0.0
debug: ^4.3.4
joi: ^17.6.4
checksum: 1cb5a4eb9022f656e1bb9a1f43d771dd058d4a4fa181b42d0e1e7ca7b5cfc42e35fad1c722be9bb6fa218398b3b0499010554a7367d2bd85eb9d7634f92546c1
languageName: node
linkType: hard
"simple-string-table@npm:^1.0.0":
version: 1.0.0
resolution: "simple-string-table@npm:1.0.0"