Feat(authentication): username password provider (#6052)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user