Feat(authentication): username password provider (#6052)

This commit is contained in:
Philip Korsholm
2024-01-23 16:04:22 +07:00
committed by GitHub
parent 9d7ed9dbaf
commit 24bb26b81a
20 changed files with 424 additions and 44 deletions
@@ -1,13 +1,14 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
MedusaModule,
Modules,
} from "@medusajs/modules-sdk"
import { IAuthenticationModuleService, ModulesSdkTypes } from "@medusajs/types"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
import { moduleDefinition } from "../module-definition"
export const initialize = async (
options?:
@@ -1,12 +1,11 @@
import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types"
import { asClass } from "awilix"
import * as defaultProviders from "@providers"
import { AuthProviderService } from "@services"
import { ServiceTypes } from "@types"
import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types"
import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix"
export default async ({
container,
options,
}: LoaderOptions<
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
@@ -17,33 +16,22 @@ export default async ({
const providersToLoad = Object.values(defaultProviders)
const authProviderService: AuthProviderService =
container.cradle["authProviderService"]
const providers = await authProviderService.list({
provider: providersToLoad.map((p) => p.PROVIDER),
})
const loadedProviders = new Map(providers.map((p) => [p.provider, p]))
const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = []
for (const provider of providersToLoad) {
container.registerAdd("providers", asClass(provider).singleton())
container.register({
[`provider_${provider.PROVIDER}`]: asClass(provider).singleton(),
})
if (loadedProviders.has(provider.PROVIDER)) {
continue
}
providersToCreate.push({
provider: provider.PROVIDER,
name: provider.DISPLAY_NAME,
[`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(),
})
}
await authProviderService.create(providersToCreate)
container.register({
[`auth_providers`]: asArray(providersToLoad),
})
}
function asArray(
resolvers: (ClassOrFunctionReturning<unknown> | Resolver<unknown>)[]
): { resolve: (container: AwilixContainer) => unknown[] } {
return {
resolve: (container: AwilixContainer) =>
resolvers.map((resolver) => container.build(resolver)),
}
}
@@ -77,6 +77,15 @@
"nullable": false,
"mappedType": "text"
},
"entity_id": {
"name": "entity_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"provider_id": {
"name": "provider_id",
"type": "text",
@@ -117,6 +126,16 @@
"name": "auth_user",
"schema": "public",
"indexes": [
{
"keyName": "IDX_auth_user_provider_entity_id",
"columnNames": [
"provider_id",
"entity_id"
],
"composite": true,
"primary": false,
"unique": true
},
{
"keyName": "auth_user_pkey",
"columnNames": [
@@ -0,0 +1,15 @@
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";');
}
}
@@ -1,25 +1,32 @@
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Entity,
Index,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import AuthProvider from "./auth-provider"
import { generateEntityId } from "@medusajs/utils"
type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata"
@Entity()
@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" })
export default class AuthUser {
[OptionalProps]: OptionalFields
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
entity_id: string
@ManyToOne(() => AuthProvider, {
joinColumn: "provider",
fieldName: "provider_id",
@@ -1,5 +1,11 @@
import {
AbstractAuthenticationModuleProvider,
AuthenticationResponse,
} from "@medusajs/types"
import { AuthUserService } from "@services"
import { AbstractAuthenticationModuleProvider } from "@medusajs/types"
import Scrypt from "scrypt-kdf"
import { isString } from "@medusajs/utils"
class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider {
public static PROVIDER = "usernamePassword"
@@ -13,8 +19,48 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider {
this.authUserSerivce_ = AuthUserService
}
async authenticate(userData: Record<string, unknown>) {
return {}
async authenticate(
userData: Record<string, any>
): Promise<AuthenticationResponse> {
const { email, password } = userData.body
if (!password || !isString(password)) {
return {
success: false,
error: "Password should be a string",
}
}
if (!email || !isString(email)) {
return {
success: false,
error: "Email should be a string",
}
}
const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
email,
UsernamePasswordProvider.PROVIDER
)
const password_hash = authUser.provider_metadata?.password
if (isString(password_hash)) {
const buf = Buffer.from(password_hash, "base64")
const success = await Scrypt.verify(buf, password)
if (success) {
delete authUser.provider_metadata!.password
return { success, authUser: JSON.parse(JSON.stringify(authUser)) }
}
}
return {
success: false,
error: "Invalid email or password",
}
}
}
@@ -1,8 +1,12 @@
import { DAL } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils"
import { AuthenticationTypes, Context, DAL, FindConfig } from "@medusajs/types"
import {
InjectManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { AuthUser } from "@models"
import { ServiceTypes } from "@types"
import { ServiceTypes, RepositoryTypes } from "@types"
type InjectedDependencies = {
authUserRepository: DAL.RepositoryService
@@ -16,8 +20,38 @@ export default class AuthUserService<
create: ServiceTypes.CreateAuthUserDTO
}
>(AuthUser)<TEntity> {
protected readonly authUserRepository_: RepositoryTypes.IAuthUserRepository<TEntity>
constructor(container: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.authUserRepository_ = container.authUserRepository
}
@InjectManager("authUserRepository_")
async retrieveByProviderAndEntityId<
TEntityMethod = AuthenticationTypes.AuthUserDTO
>(
entityId: string,
provider: string,
config: FindConfig<TEntityMethod> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
const queryConfig = ModulesSdkUtils.buildQuery<TEntity>(
{ entity_id: entityId, provider },
{ ...config, take: 1 }
)
const [result] = await this.authUserRepository_.find(
queryConfig,
sharedContext
)
if (!result) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`AuthUser with entity_id: "${entityId}" and provider: "${provider}" not found`
)
}
return result
}
}
@@ -1,9 +1,12 @@
import {
AbstractAuthenticationModuleProvider,
AuthenticationResponse,
AuthenticationTypes,
Context,
DAL,
FindConfig,
InternalModuleDeclaration,
MedusaContainer,
ModuleJoinerConfig,
} from "@medusajs/types"
@@ -11,10 +14,12 @@ import { AuthProvider, AuthUser } from "@models"
import { joinerConfig } from "../joiner-config"
import { AuthProviderService, AuthUserService } from "@services"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import {
AuthProviderDTO,
@@ -25,6 +30,7 @@ import {
FilterableAuthUserProps,
UpdateAuthUserDTO,
} from "@medusajs/types/dist/authentication/common"
import { ServiceTypes } from "@types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -37,6 +43,15 @@ export default class AuthenticationModuleService<
TAuthProvider extends AuthProvider = AuthProvider
> implements AuthenticationTypes.IAuthenticationModuleService
{
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
__hooks = {
onApplicationStart: async () => await this.createProvidersOnLoad(),
}
protected __container__: MedusaContainer
protected baseRepository_: DAL.RepositoryService
protected authUserService_: AuthUserService<TAuthUser>
@@ -50,6 +65,7 @@ export default class AuthenticationModuleService<
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.__container__ = arguments[0]
this.baseRepository_ = baseRepository
this.authUserService_ = authUserService
this.authProviderService_ = authProviderService
@@ -336,7 +352,64 @@ export default class AuthenticationModuleService<
await this.authUserService_.delete(ids, sharedContext)
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
protected getRegisteredAuthenticationProvider(
provider: string
): AbstractAuthenticationModuleProvider {
let containerProvider: AbstractAuthenticationModuleProvider
try {
containerProvider = this.__container__[`auth_provider_${provider}`]
} catch (error) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`AuthenticationProvider with for provider: ${provider} wasn't registered in the module. Have you configured your options correctly?`
)
}
return containerProvider
}
@InjectTransactionManager("baseRepository_")
async authenticate(
provider: string,
authenticationData: Record<string, unknown>,
@MedusaContext() sharedContext: Context = {}
): Promise<AuthenticationResponse> {
let registeredProvider
try {
await this.retrieveAuthProvider(provider, {})
registeredProvider = this.getRegisteredAuthenticationProvider(provider)
return await registeredProvider.authenticate(authenticationData)
} catch (error) {
return { success: false, error: error.message }
}
}
private async createProvidersOnLoad() {
const providersToLoad = this.__container__["auth_providers"]
const providers = await this.authProviderService_.list({
provider: providersToLoad.map((p) => p.provider),
})
const loadedProvidersMap = new Map(providers.map((p) => [p.provider, p]))
const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = []
for (const provider of providersToLoad) {
if (loadedProvidersMap.has(provider.provider)) {
continue
}
providersToCreate.push({
provider: provider.provider,
name: provider.displayName,
})
}
await this.authProviderService_.create(providersToCreate)
}
}
@@ -2,6 +2,7 @@ import { AuthUser } from "@models"
export type CreateAuthUserDTO = {
provider_id: string
entity_id: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown>
@@ -3,6 +3,7 @@ import { AuthProviderDTO } from "./auth-provider"
export type AuthUserDTO = {
id: string
provider_id: string
entity_id: string
provider: AuthProviderDTO
provider_metadata?: Record<string, unknown>
user_metadata: Record<string, unknown>
@@ -10,6 +11,7 @@ export type AuthUserDTO = {
}
export type CreateAuthUserDTO = {
entity_id: string
provider_id: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>