Revamp the authentication setup (#7419)
* feat: Add email pass authentication provider package * feat: Revamp auth module and remove concept of scope * feat: Revamp the auth module to be more standardized in how providers are loaded * feat: Switch from scope to actor type for authentication * feat: Add support for per-actor auth methods * feat: Add emailpass auth provider by default * fix: Add back app_metadata in auth module
This commit is contained in:
@@ -33,7 +33,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
},
|
||||
async () => {
|
||||
return await api.post("/auth/admin/emailpass", {
|
||||
return await api.post("/auth/user/emailpass", {
|
||||
email: "admin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
@@ -63,7 +63,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("should test the entire authentication lifecycle", async () => {
|
||||
// sign in
|
||||
const response = await api.post("/auth/admin/emailpass", {
|
||||
const response = await api.post("/auth/user/emailpass", {
|
||||
email: "admin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ medusaIntegrationTestRunner({
|
||||
token = await breaking(
|
||||
() => null,
|
||||
async () => {
|
||||
const emailPassResponse = await api.post("/auth/admin/emailpass", {
|
||||
const emailPassResponse = await api.post("/auth/user/emailpass", {
|
||||
email: "test@test123.com",
|
||||
password: "test123",
|
||||
})
|
||||
@@ -230,7 +230,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
// V2 only test
|
||||
it.skip("should throw, if session/bearer auth is present for existing user", async () => {
|
||||
const emailPassResponse = await api.post("/auth/admin/emailpass", {
|
||||
const emailPassResponse = await api.post("/auth/user/emailpass", {
|
||||
email: "test@test123.com",
|
||||
password: "test123",
|
||||
})
|
||||
|
||||
@@ -41,22 +41,7 @@ module.exports = {
|
||||
options: { ttl: cacheTTL },
|
||||
},
|
||||
workflows: true,
|
||||
[Modules.AUTH]: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
resolve: "@medusajs/auth",
|
||||
options: {
|
||||
providers: [
|
||||
{
|
||||
name: "emailpass",
|
||||
scopes: {
|
||||
admin: {},
|
||||
store: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[Modules.AUTH]: true,
|
||||
[Modules.USER]: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
|
||||
@@ -32,7 +32,6 @@ export const createAdminUser = async (
|
||||
const authIdentity = await authModule.create({
|
||||
provider: "emailpass",
|
||||
entity_id: "admin@medusa.js",
|
||||
scope: "admin",
|
||||
provider_metadata: {
|
||||
password: "somepassword",
|
||||
},
|
||||
@@ -55,8 +54,6 @@ export const createAdminUser = async (
|
||||
actor_id: user.id,
|
||||
actor_type: "user",
|
||||
auth_identity_id: authIdentity.id,
|
||||
scope: "admin",
|
||||
app_metadata: {},
|
||||
},
|
||||
"test"
|
||||
)
|
||||
|
||||
@@ -43,14 +43,13 @@ medusaIntegrationTestRunner({
|
||||
await authService.create({
|
||||
provider: "emailpass",
|
||||
entity_id: email,
|
||||
scope: "admin",
|
||||
provider_metadata: {
|
||||
password: passwordHash,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api
|
||||
.post(`/auth/admin/emailpass`, {
|
||||
.post(`/auth/user/emailpass`, {
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
@@ -75,14 +74,13 @@ medusaIntegrationTestRunner({
|
||||
await authService.create({
|
||||
provider: "emailpass",
|
||||
entity_id: email,
|
||||
scope: "admin",
|
||||
provider_metadata: {
|
||||
password: passwordHash,
|
||||
},
|
||||
})
|
||||
|
||||
const error = await api
|
||||
.post(`/auth/admin/emailpass`, {
|
||||
.post(`/auth/user/emailpass`, {
|
||||
email: email,
|
||||
password: "incorrect-password",
|
||||
})
|
||||
@@ -101,7 +99,7 @@ medusaIntegrationTestRunner({
|
||||
).toString("base64")
|
||||
|
||||
const error = await api
|
||||
.post(`/auth/admin/emailpass`, {
|
||||
.post(`/auth/user/emailpass`, {
|
||||
email: "should-not-exist",
|
||||
password: "should-not-exist",
|
||||
})
|
||||
|
||||
@@ -38,7 +38,6 @@ medusaIntegrationTestRunner({
|
||||
const authIdentity = await authService.create({
|
||||
entity_id: "store_user",
|
||||
provider: "emailpass",
|
||||
scope: "store",
|
||||
})
|
||||
|
||||
const token = jwt.sign(authIdentity, http.jwtSecret)
|
||||
|
||||
@@ -27,7 +27,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should fail to accept an invite with an invalid invite token", async () => {
|
||||
const authResponse = await api.post(`/auth/admin/emailpass`, {
|
||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
||||
email: "potential_member@test.com",
|
||||
password: "supersecret",
|
||||
})
|
||||
@@ -58,7 +58,7 @@ medusaIntegrationTestRunner({
|
||||
email: "potential_member@test.com",
|
||||
})
|
||||
|
||||
const authResponse = await api.post(`/auth/admin/emailpass`, {
|
||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
||||
email: "potential_member@test.com",
|
||||
password: "supersecret",
|
||||
})
|
||||
@@ -92,7 +92,7 @@ medusaIntegrationTestRunner({
|
||||
email: "potential_member@test.com",
|
||||
})
|
||||
|
||||
const authResponse = await api.post(`/auth/admin/emailpass`, {
|
||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
||||
email: "some-email@test.com",
|
||||
password: "supersecret",
|
||||
})
|
||||
|
||||
@@ -25,7 +25,6 @@ export const createAuthenticatedCustomer = async (
|
||||
const authIdentity = await authService.create({
|
||||
entity_id: "store_user",
|
||||
provider: "emailpass",
|
||||
scope: "store",
|
||||
})
|
||||
|
||||
// Ideally we simulate a signup process than manually linking here.
|
||||
@@ -45,8 +44,6 @@ export const createAuthenticatedCustomer = async (
|
||||
actor_id: customer.id,
|
||||
actor_type: "customer",
|
||||
auth_identity_id: authIdentity.id,
|
||||
scope: "store",
|
||||
app_metadata: {},
|
||||
},
|
||||
http.jwtSecret
|
||||
)
|
||||
|
||||
@@ -47,22 +47,7 @@ module.exports = {
|
||||
medusa_v2: enableMedusaV2,
|
||||
},
|
||||
modules: {
|
||||
[Modules.AUTH]: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
resolve: "@medusajs/auth",
|
||||
options: {
|
||||
providers: [
|
||||
{
|
||||
name: "emailpass",
|
||||
scopes: {
|
||||
admin: {},
|
||||
store: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[Modules.AUTH]: true,
|
||||
[Modules.USER]: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
|
||||
@@ -5,7 +5,7 @@ status_code=$(curl \
|
||||
-H "Content-Type: application/json"\
|
||||
-d '{"email":"'$1'", "password":"'$2'"}'\
|
||||
--write-out %{http_code}\
|
||||
http://localhost:9000/auth/admin/emailpass)
|
||||
http://localhost:9000/auth/user/emailpass)
|
||||
|
||||
if [[ "$status_code" -ne 200 ]] ; then
|
||||
echo "Site status changed to $status_code"
|
||||
|
||||
@@ -7,7 +7,7 @@ export const useEmailPassLogin = (
|
||||
options?: UseMutationOptions<void, Error, EmailPassReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => sdk.auth.login("admin", "emailpass", payload),
|
||||
mutationFn: (payload) => sdk.auth.login("user", "emailpass", payload),
|
||||
onSuccess: async (data, variables, context) => {
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
@@ -26,7 +26,7 @@ export const useCreateAuthUser = (
|
||||
options?: UseMutationOptions<{ token: string }, Error, EmailPassReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => sdk.auth.create("admin", "emailpass", payload),
|
||||
mutationFn: (payload) => sdk.auth.create("user", "emailpass", payload),
|
||||
onSuccess: async (data, variables, context) => {
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
|
||||
@@ -10,13 +10,13 @@ export class Auth {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
public login = async (
|
||||
scope: "admin" | "store",
|
||||
login = async (
|
||||
actor: "customer" | "user",
|
||||
method: "emailpass",
|
||||
payload: { email: string; password: string }
|
||||
) => {
|
||||
const { token } = await this.client.fetch<{ token: string }>(
|
||||
`/auth/${scope}/${method}`,
|
||||
`/auth/${actor}/${method}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: payload,
|
||||
@@ -45,11 +45,11 @@ export class Auth {
|
||||
}
|
||||
|
||||
create = async (
|
||||
scope: "admin" | "store",
|
||||
actor: "customer" | "user",
|
||||
method: "emailpass",
|
||||
payload: { email: string; password: string }
|
||||
): Promise<{ token: string }> => {
|
||||
return await this.client.fetch(`/auth/${scope}/${method}`, {
|
||||
return await this.client.fetch(`/auth/${actor}/${method}`, {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
})
|
||||
|
||||
@@ -22,12 +22,6 @@ export type AuthIdentityDTO = {
|
||||
*/
|
||||
entity_id: string
|
||||
|
||||
/**
|
||||
* The scope of the auth identity. For example,
|
||||
* `admin` or `store`.
|
||||
*/
|
||||
scope: string
|
||||
|
||||
/**
|
||||
* Holds custom data related to the provider in key-value pairs.
|
||||
*/
|
||||
@@ -37,11 +31,6 @@ export type AuthIdentityDTO = {
|
||||
* Holds custom data related to the user in key-value pairs.
|
||||
*/
|
||||
user_metadata: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Holds custom data related to the third-party app in key-value pairs.
|
||||
*/
|
||||
app_metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,12 +56,6 @@ export type CreateAuthIdentityDTO = {
|
||||
*/
|
||||
entity_id: string
|
||||
|
||||
/**
|
||||
* The scope of the auth identity. For example,
|
||||
* `admin` or `store`.
|
||||
*/
|
||||
scope: string
|
||||
|
||||
/**
|
||||
* Holds custom data related to the provider in key-value pairs.
|
||||
*/
|
||||
@@ -82,11 +65,6 @@ export type CreateAuthIdentityDTO = {
|
||||
* Holds custom data related to the user in key-value pairs.
|
||||
*/
|
||||
user_metadata?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Holds custom data related to the third-party app in key-value pairs.
|
||||
*/
|
||||
app_metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,11 +87,6 @@ export type UpdateAuthIdentityDTO = {
|
||||
* Holds custom data related to the user in key-value pairs.
|
||||
*/
|
||||
user_metadata?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Holds custom data related to the third-party app in key-value pairs.
|
||||
*/
|
||||
app_metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,31 +42,6 @@ export type AuthenticationResponse = {
|
||||
successRedirectUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*
|
||||
* The configurations of the `providers` option
|
||||
* passed to the Auth Module.
|
||||
*/
|
||||
export type AuthModuleProviderConfig = {
|
||||
/**
|
||||
* The provider's name.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The scopes configuration of that provider.
|
||||
*/
|
||||
scopes: Record<string, AuthProviderScope>
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*
|
||||
* The scope configurations of an auth provider.
|
||||
*/
|
||||
export type AuthProviderScope = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*
|
||||
@@ -77,30 +52,25 @@ export type AuthenticationInput = {
|
||||
/**
|
||||
* URL of the incoming authentication request.
|
||||
*/
|
||||
url: string
|
||||
url?: string
|
||||
|
||||
/**
|
||||
* Headers of incoming authentication request.
|
||||
*/
|
||||
headers: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Query params of the incoming authentication request.
|
||||
*/
|
||||
query: Record<string, string>
|
||||
query?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Body of the incoming authentication request.
|
||||
*/
|
||||
body: Record<string, string>
|
||||
|
||||
/**
|
||||
* Scope for the authentication request.
|
||||
*/
|
||||
authScope: string
|
||||
body?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Protocol of the incoming authentication request (For example, `https`).
|
||||
*/
|
||||
protocol: string
|
||||
protocol?: string
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./service"
|
||||
export * from "./common"
|
||||
export * from "./provider"
|
||||
export * from "./providers"
|
||||
|
||||
32
packages/core/types/src/auth/provider.ts
Normal file
32
packages/core/types/src/auth/provider.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
AuthIdentityDTO,
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
CreateAuthIdentityDTO,
|
||||
} from "./common"
|
||||
|
||||
export interface AuthIdentityProviderService {
|
||||
// The provider is injected by the auth identity module
|
||||
retrieve: (selector: {
|
||||
entity_id: string
|
||||
provider: string
|
||||
}) => Promise<AuthIdentityDTO>
|
||||
create: (data: CreateAuthIdentityDTO) => Promise<AuthIdentityDTO>
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Overview
|
||||
*
|
||||
* Authentication provider interface for the auth module.
|
||||
*
|
||||
*/
|
||||
export interface IAuthProvider {
|
||||
authenticate(
|
||||
data: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse>
|
||||
validateCallback(
|
||||
data: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse>
|
||||
}
|
||||
7
packages/core/types/src/auth/providers/emailpass.ts
Normal file
7
packages/core/types/src/auth/providers/emailpass.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface EmailPassAuthProviderOptions {
|
||||
hashConfig?: {
|
||||
logN: number
|
||||
r: number
|
||||
p: number
|
||||
}
|
||||
}
|
||||
1
packages/core/types/src/auth/providers/index.ts
Normal file
1
packages/core/types/src/auth/providers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./emailpass"
|
||||
@@ -43,7 +43,6 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* headers: req.headers,
|
||||
* query: req.query,
|
||||
* body: req.body,
|
||||
* authScope: "admin",
|
||||
* protocol: req.protocol,
|
||||
* } as AuthenticationInput)
|
||||
* ```
|
||||
@@ -81,7 +80,6 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* headers: req.headers,
|
||||
* query: req.query,
|
||||
* body: req.body,
|
||||
* authScope: "admin",
|
||||
* protocol: req.protocol,
|
||||
* } as AuthenticationInput)
|
||||
* ```
|
||||
@@ -200,12 +198,10 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* {
|
||||
* provider: "emailpass",
|
||||
* entity_id: "user@example.com",
|
||||
* scope: "admin",
|
||||
* },
|
||||
* {
|
||||
* provider: "google",
|
||||
* entity_id: "user@gmail.com",
|
||||
* scope: "email profile",
|
||||
* },
|
||||
* ])
|
||||
*/
|
||||
@@ -225,7 +221,6 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* const authIdentity = await authModuleService.create({
|
||||
* provider: "emailpass",
|
||||
* entity_id: "user@example.com",
|
||||
* scope: "admin",
|
||||
* })
|
||||
*/
|
||||
create(
|
||||
@@ -244,9 +239,6 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* const authIdentities = await authModuleService.update([
|
||||
* {
|
||||
* id: "authusr_123",
|
||||
* app_metadata: {
|
||||
* test: true,
|
||||
* },
|
||||
* },
|
||||
* ])
|
||||
*/
|
||||
@@ -265,9 +257,6 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* @example
|
||||
* const authIdentity = await authModuleService.update({
|
||||
* id: "authusr_123",
|
||||
* app_metadata: {
|
||||
* test: true,
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
update(
|
||||
|
||||
@@ -705,6 +705,35 @@ export type ProjectConfigOptions = {
|
||||
* ```
|
||||
*/
|
||||
adminCors: string
|
||||
|
||||
/**
|
||||
* Optionally you can specify the supported authentication providers per actor type (such as user, customer, or any custom actors).
|
||||
* For example, you only want to allow SSO logins for `users` to the admin, while you want to allow email/password logins for `customers` to the storefront.
|
||||
*
|
||||
* `authMethodsPerActor` is a a map where the actor type (eg. 'user') is the key, and an array of supported auth providers as the value.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* Some example values of common use cases:
|
||||
*
|
||||
* Then, set the configuration in `medusa-config.js`:
|
||||
*
|
||||
* ```js title="medusa-config.js"
|
||||
* module.exports = {
|
||||
* projectConfig: {
|
||||
* http: {
|
||||
* authMethodsPerActor: {
|
||||
* user: ['sso'],
|
||||
* customer: ["emailpass", "google"]
|
||||
* },
|
||||
* },
|
||||
* // ...
|
||||
* },
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
authMethodsPerActor: Record<string, string[]>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { AuthProviderScope, AuthenticationResponse } from "@medusajs/types"
|
||||
import {
|
||||
AuthIdentityProviderService,
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
IAuthProvider,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { MedusaError } from "../common"
|
||||
|
||||
export abstract class AbstractAuthModuleProvider {
|
||||
public static PROVIDER: string
|
||||
public static DISPLAY_NAME: string
|
||||
export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
||||
private static PROVIDER: string
|
||||
private static DISPLAY_NAME: string
|
||||
|
||||
protected readonly container_: any
|
||||
protected scopeConfig_: AuthProviderScope
|
||||
protected scope_: string
|
||||
|
||||
private readonly scopes_: Record<string, AuthProviderScope>
|
||||
|
||||
public get provider() {
|
||||
return (this.constructor as typeof AbstractAuthModuleProvider).PROVIDER
|
||||
}
|
||||
@@ -20,43 +18,22 @@ export abstract class AbstractAuthModuleProvider {
|
||||
return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
{ scopes },
|
||||
config: { provider: string; displayName: string }
|
||||
) {
|
||||
protected constructor({}, config: { provider: string; displayName: string }) {
|
||||
this.container_ = arguments[0]
|
||||
this.scopes_ = scopes
|
||||
;(this.constructor as typeof AbstractAuthModuleProvider).PROVIDER ??=
|
||||
config.provider
|
||||
;(this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME ??=
|
||||
config.displayName
|
||||
}
|
||||
|
||||
private validateScope(scope) {
|
||||
if (!this.scopes_[scope]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
`Scope "${scope}" is not valid for provider ${this.provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public withScope(scope: string) {
|
||||
this.validateScope(scope)
|
||||
|
||||
const cloned = new (this.constructor as any)(this.container_)
|
||||
cloned.scope_ = scope
|
||||
cloned.scopeConfig_ = this.scopes_[scope]
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
abstract authenticate(
|
||||
data: Record<string, unknown>
|
||||
data: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse>
|
||||
|
||||
public validateCallback(
|
||||
data: Record<string, unknown>
|
||||
validateCallback(
|
||||
data: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
throw new Error(
|
||||
`Callback authentication not implemented for provider ${this.provider}`
|
||||
|
||||
@@ -16,7 +16,7 @@ import { createLinkBody } from "../../utils/validators"
|
||||
export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/api-keys*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/campaigns*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -15,7 +15,7 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/collections*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ export const adminCurrencyRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/currencies*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -15,7 +15,7 @@ export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/customer-groups*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -19,7 +19,7 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/customers*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -13,7 +13,7 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/draft-orders*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -8,7 +8,7 @@ export const adminFulfillmentProvidersRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/fulfillment-providers*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/fulfillment-sets*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminFulfillmentsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/fulfillments*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -20,7 +20,7 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/inventory-items*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -18,7 +18,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/admin/invites",
|
||||
middlewares: [
|
||||
authenticate("admin", ["session", "bearer", "api-key"]),
|
||||
authenticate("user", ["session", "bearer", "api-key"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetInvitesParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
@@ -29,7 +29,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["POST"],
|
||||
matcher: "/admin/invites",
|
||||
middlewares: [
|
||||
authenticate("admin", ["session", "bearer", "api-key"]),
|
||||
authenticate("user", ["session", "bearer", "api-key"]),
|
||||
validateAndTransformBody(AdminCreateInvite),
|
||||
validateAndTransformQuery(
|
||||
AdminGetInviteParams,
|
||||
@@ -41,7 +41,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: "POST",
|
||||
matcher: "/admin/invites/accept",
|
||||
middlewares: [
|
||||
authenticate("admin", ["session", "bearer"], {
|
||||
authenticate("user", ["session", "bearer"], {
|
||||
allowUnregistered: true,
|
||||
}),
|
||||
validateAndTransformBody(AdminInviteAccept),
|
||||
@@ -55,7 +55,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/admin/invites/:id",
|
||||
middlewares: [
|
||||
authenticate("admin", ["session", "bearer", "api-key"]),
|
||||
authenticate("user", ["session", "bearer", "api-key"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetInviteParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
@@ -65,13 +65,13 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/invites/:id",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
matcher: "/admin/invites/:id/resend",
|
||||
middlewares: [
|
||||
authenticate("admin", ["session", "bearer", "api-key"]),
|
||||
authenticate("user", ["session", "bearer", "api-key"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetInviteParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/orders*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -16,7 +16,7 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/payments",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -18,7 +18,7 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/price-lists*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
export const adminPricingRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/pricing*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -15,7 +15,7 @@ export const adminProductCategoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/product-categories*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminProductTypeRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/product-types/*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/products*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/promotions*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/regions*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -15,7 +15,7 @@ export const adminReservationRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/reservations*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -13,7 +13,7 @@ export const adminReturnReasonRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/return-reasons*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -13,7 +13,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/returns*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -16,7 +16,7 @@ export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/sales-channels*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -21,7 +21,7 @@ import { createBatchBody } from "../../utils/validators"
|
||||
export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/shipping-options*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
export const adminShippingProfilesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/admin/shipping-profiles*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -17,7 +17,7 @@ export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/stock-locations*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -13,7 +13,7 @@ export const adminStoreRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/stores*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -17,7 +17,7 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/tax-rates*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/tax-regions*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@@ -14,7 +14,7 @@ export const adminUploadRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/uploads*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
// TODO: There is a `/protected` route in v1 that might need a bit more thought when implementing
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/admin/users",
|
||||
middlewares: [
|
||||
authenticate("admin", ["bearer", "session"]),
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUsersParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
@@ -28,7 +28,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["POST"],
|
||||
matcher: "/admin/users",
|
||||
middlewares: [
|
||||
authenticate("admin", ["bearer", "session"], { allowUnregistered: true }),
|
||||
authenticate("user", ["bearer", "session"], { allowUnregistered: true }),
|
||||
validateAndTransformBody(AdminCreateUser),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
@@ -40,7 +40,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/admin/users/:id",
|
||||
middlewares: [
|
||||
authenticate("admin", ["bearer", "session"]),
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
@@ -51,7 +51,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/admin/users/me",
|
||||
middlewares: [
|
||||
authenticate("admin", ["bearer", "session"]),
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
@@ -62,7 +62,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["POST"],
|
||||
matcher: "/admin/users/:id",
|
||||
middlewares: [
|
||||
authenticate("admin", ["bearer", "session"]),
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformBody(AdminUpdateUser),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
@@ -73,6 +73,6 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/users/:id",
|
||||
middlewares: [authenticate("admin", ["bearer", "session"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session"])],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -73,8 +73,6 @@ export const POST = async (
|
||||
actor_id: result.id,
|
||||
actor_type: "user",
|
||||
auth_identity_id: req.auth_context.auth_identity_id,
|
||||
app_metadata: {},
|
||||
scope: "admin",
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
|
||||
@@ -16,7 +16,7 @@ export const adminWorkflowsExecutionsMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["ALL"],
|
||||
matcher: "/admin/workflows-executions*",
|
||||
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||
middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
IAuthModuleService,
|
||||
ConfigModule,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
@@ -9,10 +13,23 @@ import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
import { generateJwtToken } from "../../../../utils/auth/token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { scope, auth_provider } = req.params
|
||||
const actorType = scope === "admin" ? "user" : "customer"
|
||||
|
||||
const { actor_type, auth_provider } = req.params
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const config: ConfigModule = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethods = (config.projectConfig?.http as any)?.authMethods ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethods[actor_type]) {
|
||||
if (!authMethods[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
@@ -22,7 +39,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
headers: req.headers,
|
||||
query: req.query,
|
||||
body: req.body,
|
||||
authScope: scope,
|
||||
protocol: req.protocol,
|
||||
} as AuthenticationInput
|
||||
|
||||
@@ -31,11 +47,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "auth_identity",
|
||||
fields: [`${actorType}.id`],
|
||||
fields: [`${actor_type}.id`],
|
||||
variables: { id: authIdentity.id },
|
||||
})
|
||||
const [actorData] = await remoteQuery(queryObject)
|
||||
const entityId = actorData?.[actorType]?.id
|
||||
const entityId = actorData?.[actor_type]?.id
|
||||
|
||||
if (success) {
|
||||
const { http } = req.scope.resolve(
|
||||
@@ -43,14 +59,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
).projectConfig
|
||||
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
// TODO: Clean up mapping between scope and actor type
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: entityId,
|
||||
actor_type: actorType,
|
||||
actor_type,
|
||||
auth_identity_id: authIdentity.id,
|
||||
app_metadata: {},
|
||||
scope,
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
IAuthModuleService,
|
||||
ConfigModule,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
@@ -9,9 +13,22 @@ import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||
import { generateJwtToken } from "../../../utils/auth/token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { scope, auth_provider } = req.params
|
||||
const actorType = scope === "admin" ? "user" : "customer"
|
||||
const { actor_type, auth_provider } = req.params
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const config: ConfigModule = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethods = (config.projectConfig?.http as any)?.authMethods ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethods[actor_type]) {
|
||||
if (!authMethods[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
@@ -22,7 +39,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
headers: req.headers,
|
||||
query: req.query,
|
||||
body: req.body,
|
||||
authScope: scope,
|
||||
protocol: req.protocol,
|
||||
} as AuthenticationInput
|
||||
|
||||
@@ -43,21 +59,18 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "auth_identity",
|
||||
fields: [`${actorType}.id`],
|
||||
fields: [`${actor_type}.id`],
|
||||
variables: { id: authIdentity.id },
|
||||
})
|
||||
const [actorData] = await remoteQuery(queryObject)
|
||||
const entityId = actorData?.[actorType]?.id
|
||||
const entityId = actorData?.[actor_type]?.id
|
||||
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
// TODO: Clean up mapping between scope and actor type
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: entityId,
|
||||
actor_type: actorType,
|
||||
actor_type,
|
||||
auth_identity_id: authIdentity.id,
|
||||
app_metadata: {},
|
||||
scope,
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
@@ -14,12 +14,12 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:scope/:auth_provider/callback",
|
||||
matcher: "/auth/:actor_type/:auth_provider/callback",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:scope/:auth_provider",
|
||||
matcher: "/auth/:actor_type/:auth_provider",
|
||||
middlewares: [],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -25,7 +25,7 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: "ALL",
|
||||
matcher: "/store/carts*",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"], {
|
||||
authenticate("customer", ["session", "bearer"], {
|
||||
allowUnauthenticated: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -20,7 +20,9 @@ export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["POST"],
|
||||
matcher: "/store/customers",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"], { allowUnregistered: true }),
|
||||
authenticate("customer", ["session", "bearer"], {
|
||||
allowUnregistered: true,
|
||||
}),
|
||||
validateAndTransformBody(StoreCreateCustomer),
|
||||
validateAndTransformQuery(
|
||||
StoreGetCustomerParams,
|
||||
@@ -31,7 +33,7 @@ export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/store/customers/me*",
|
||||
middlewares: [authenticate("store", ["session", "bearer"])],
|
||||
middlewares: [authenticate("customer", ["session", "bearer"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
|
||||
@@ -41,8 +41,6 @@ export const POST = async (
|
||||
actor_id: result.id,
|
||||
actor_type: "customer",
|
||||
auth_identity_id: req.auth_context.auth_identity_id,
|
||||
app_metadata: {},
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/store/orders",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"]),
|
||||
authenticate("customer", ["session", "bearer"]),
|
||||
validateAndTransformQuery(
|
||||
StoreGetOrdersParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
@@ -20,7 +20,7 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: ["GET"],
|
||||
matcher: "/store/orders/:id",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"], {
|
||||
authenticate("customer", ["session", "bearer"], {
|
||||
allowUnauthenticated: true,
|
||||
}),
|
||||
validateAndTransformQuery(
|
||||
|
||||
@@ -14,7 +14,7 @@ export const storePaymentCollectionsMiddlewares: MiddlewareRoute[] = [
|
||||
method: "ALL",
|
||||
matcher: "/store/payment-collections*",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"], {
|
||||
authenticate("customer", ["session", "bearer"], {
|
||||
allowUnauthenticated: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
method: "ALL",
|
||||
matcher: "/store/products*",
|
||||
middlewares: [
|
||||
authenticate("store", ["session", "bearer"], {
|
||||
authenticate("customer", ["session", "bearer"], {
|
||||
allowUnauthenticated: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -46,14 +46,18 @@ export default async function ({
|
||||
} else {
|
||||
const user = await userService.create({ email })
|
||||
|
||||
const { authIdentity } = await authService.authenticate(provider, {
|
||||
const { authIdentity, error } = await authService.authenticate(provider, {
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
authScope: "admin",
|
||||
})
|
||||
|
||||
if (error) {
|
||||
Logger.error(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.USER]: {
|
||||
|
||||
@@ -61,11 +61,8 @@ export interface MedusaRequest<Body = unknown>
|
||||
|
||||
export interface AuthContext {
|
||||
actor_id: string
|
||||
// TODO: We possibly want to make this more open-ended so it's easy to extend.
|
||||
actor_type: "api-key" | "user" | "customer" | "unknown"
|
||||
actor_type: string
|
||||
auth_identity_id: string
|
||||
scope: string
|
||||
app_metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface AuthenticatedMedusaRequest<Body = never>
|
||||
|
||||
@@ -8,25 +8,23 @@ import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../types/routing"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
const SESSION_AUTH = "session"
|
||||
const BEARER_AUTH = "bearer"
|
||||
const API_KEY_AUTH = "api-key"
|
||||
|
||||
// This is the only hard-coded actor type, as API keys have special handling for now. We could also generalize API keys to carry the actor type with them.
|
||||
const ADMIN_ACTOR_TYPE = "user"
|
||||
|
||||
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
|
||||
|
||||
const ADMIN_SCOPE = "admin"
|
||||
const STORE_SCOPE = "store"
|
||||
const ALL_SCOPE = "*"
|
||||
|
||||
type Scope = typeof ADMIN_SCOPE | typeof STORE_SCOPE | typeof ALL_SCOPE
|
||||
|
||||
type MedusaSession = {
|
||||
auth_context: AuthContext
|
||||
}
|
||||
|
||||
export const authenticate = (
|
||||
authScope: Scope | Scope[],
|
||||
actorType: string,
|
||||
authType: AuthType | AuthType[],
|
||||
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
|
||||
): RequestHandler => {
|
||||
@@ -36,21 +34,20 @@ export const authenticate = (
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const authTypes = Array.isArray(authType) ? authType : [authType]
|
||||
const scopes = Array.isArray(authScope) ? authScope : [authScope]
|
||||
const actorTypes = Array.isArray(actorType) ? actorType : [actorType]
|
||||
const req_ = req as AuthenticatedMedusaRequest
|
||||
|
||||
// We only allow authenticating using a secret API key on the admin
|
||||
const isExclusivelyAdmin =
|
||||
scopes.length === 1 && scopes.includes(ADMIN_SCOPE)
|
||||
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyAdmin) {
|
||||
const isExclusivelyUser =
|
||||
actorTypes.length === 1 && actorTypes[0] === ADMIN_ACTOR_TYPE
|
||||
|
||||
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyUser) {
|
||||
const apiKey = await getApiKeyInfo(req)
|
||||
if (apiKey) {
|
||||
req_.auth_context = {
|
||||
actor_id: apiKey.id,
|
||||
actor_type: "api-key",
|
||||
auth_identity_id: "",
|
||||
app_metadata: {},
|
||||
scope: ADMIN_SCOPE,
|
||||
}
|
||||
|
||||
return next()
|
||||
@@ -61,27 +58,29 @@ export const authenticate = (
|
||||
let authContext: AuthContext | null = getAuthContextFromSession(
|
||||
req.session,
|
||||
authTypes,
|
||||
scopes
|
||||
actorTypes
|
||||
)
|
||||
|
||||
if (!authContext) {
|
||||
const { http } =
|
||||
req.scope.resolve<ConfigModule>("configModule").projectConfig
|
||||
const { http } = req.scope.resolve<ConfigModule>(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
authContext = getAuthContextFromJwtToken(
|
||||
req.headers.authorization,
|
||||
http.jwtSecret!,
|
||||
authTypes,
|
||||
scopes
|
||||
actorTypes
|
||||
)
|
||||
}
|
||||
|
||||
// If the entity is authenticated, and it is a registered user/customer we can continue
|
||||
if (!!authContext?.actor_id && authContext.actor_type !== "unknown") {
|
||||
// If the entity is authenticated, and it is a registered actor we can continue
|
||||
if (authContext?.actor_id) {
|
||||
req_.auth_context = authContext
|
||||
return next()
|
||||
}
|
||||
|
||||
// If the entity is authenticated, but there is no user/customer yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
|
||||
// If the entity is authenticated, but there is no registered actor yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
|
||||
if (authContext?.auth_identity_id && options.allowUnregistered) {
|
||||
req_.auth_context = authContext
|
||||
return next()
|
||||
@@ -142,7 +141,7 @@ const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
|
||||
const getAuthContextFromSession = (
|
||||
session: Partial<MedusaSession> = {},
|
||||
authTypes: AuthType[],
|
||||
scopes: Scope[]
|
||||
actorTypes: string[]
|
||||
): AuthContext | null => {
|
||||
if (!authTypes.includes(SESSION_AUTH)) {
|
||||
return null
|
||||
@@ -150,8 +149,8 @@ const getAuthContextFromSession = (
|
||||
|
||||
if (
|
||||
session.auth_context &&
|
||||
(scopes.includes("*") ||
|
||||
scopes.includes(session.auth_context.scope as Scope))
|
||||
(actorTypes.includes("*") ||
|
||||
actorTypes.includes(session.auth_context.actor_type))
|
||||
) {
|
||||
return session.auth_context
|
||||
}
|
||||
@@ -163,7 +162,7 @@ const getAuthContextFromJwtToken = (
|
||||
authHeader: string | undefined,
|
||||
jwtSecret: string,
|
||||
authTypes: AuthType[],
|
||||
scopes: Scope[]
|
||||
actorTypes: string[]
|
||||
): AuthContext | null => {
|
||||
if (!authTypes.includes(BEARER_AUTH)) {
|
||||
return null
|
||||
@@ -185,7 +184,10 @@ const getAuthContextFromJwtToken = (
|
||||
// verify token and set authUser
|
||||
try {
|
||||
const verified = jwt.verify(token, jwtSecret) as JwtPayload
|
||||
if (scopes.includes("*") || scopes.includes(verified.scope)) {
|
||||
if (
|
||||
actorTypes.includes("*") ||
|
||||
actorTypes.includes(verified.actor_type)
|
||||
) {
|
||||
return verified as AuthContext
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,18 +8,15 @@ export async function createAuthIdentities(
|
||||
id: "test-id",
|
||||
entity_id: "test-id",
|
||||
provider: "manual",
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
id: "test-id-1",
|
||||
entity_id: "test-id-1",
|
||||
provider: "manual",
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
entity_id: "test-id-2",
|
||||
provider: "store",
|
||||
scope: "store",
|
||||
},
|
||||
]
|
||||
): Promise<AuthIdentity[]> {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
AuthIdentityDTO,
|
||||
AuthIdentityProviderService,
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
|
||||
export class AuthServiceFixtures extends AbstractAuthModuleProvider {
|
||||
constructor() {
|
||||
super(
|
||||
{},
|
||||
{ provider: "plaintextpass", displayName: "plaintextpass Fixture" }
|
||||
)
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
authenticationData: AuthenticationInput,
|
||||
service: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
const { email, password } = authenticationData.body ?? {}
|
||||
let authIdentity: AuthIdentityDTO | undefined
|
||||
try {
|
||||
authIdentity = await service.retrieve({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
})
|
||||
|
||||
if (authIdentity.provider_metadata?.password === password) {
|
||||
return {
|
||||
success: true,
|
||||
authIdentity,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
const createdAuthIdentity = await service.create({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
provider_metadata: {
|
||||
password,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authIdentity: createdAuthIdentity,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const services = [AuthServiceFixtures]
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./default-provider"
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { createAuthIdentities } from "../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -216,7 +216,6 @@ moduleIntegrationTestRunner({
|
||||
id: "test",
|
||||
provider: "manual",
|
||||
entity_id: "test",
|
||||
scope: "store",
|
||||
},
|
||||
])
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { resolve } from "path"
|
||||
|
||||
let moduleOptions = {
|
||||
providers: [
|
||||
{
|
||||
resolve: resolve(
|
||||
process.cwd() +
|
||||
"/integration-tests/__fixtures__/providers/default-provider"
|
||||
),
|
||||
options: {
|
||||
config: {
|
||||
plaintextpass: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
moduleOptions,
|
||||
testSuite: ({ service }: SuiteOptions<IAuthModuleService>) =>
|
||||
describe("Auth Module Service", () => {
|
||||
beforeEach(async () => {
|
||||
await service.create({
|
||||
entity_id: "test@admin.com",
|
||||
provider: "plaintextpass",
|
||||
provider_metadata: {
|
||||
password: "plaintext",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("it fails if the provider does not exist", async () => {
|
||||
const err = await service
|
||||
.authenticate("facebook", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "password",
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toEqual({
|
||||
success: false,
|
||||
error: "Could not find a auth provider with id: facebook",
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully calls the provider for authentication if correct password", async () => {
|
||||
const result = await service.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "plaintext",
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
authIdentity: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
entity_id: "test@admin.com",
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail if the password is incorrect", async () => {
|
||||
const result = await service
|
||||
.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "incorrect",
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully create a new entity if nonexistent", async () => {
|
||||
const result = await service.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "new@admin.com",
|
||||
password: "newpass",
|
||||
},
|
||||
})
|
||||
|
||||
const dbAuthIdentity = await service.retrieve(result.authIdentity.id)
|
||||
|
||||
expect(dbAuthIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
entity_id: "new@admin.com",
|
||||
})
|
||||
)
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -1,231 +0,0 @@
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthIdentity Service", () => {
|
||||
beforeEach(async () => {
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager())
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should list authIdentities", async () => {
|
||||
const authIdentities = await service.list()
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "store",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should list authIdentities by id", async () => {
|
||||
const authIdentities = await service.list({
|
||||
id: ["test-id"],
|
||||
})
|
||||
|
||||
expect(authIdentities).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should list authIdentities by provider_id", async () => {
|
||||
const authIdentities = await service.list({
|
||||
provider: "manual",
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-id-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
it("should list authIdentities", async () => {
|
||||
const [authIdentities, count] = await service.listAndCount()
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(count).toEqual(3)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "store",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should listAndCount authIdentities by provider_id", async () => {
|
||||
const [authIdentities, count] = await service.listAndCount({
|
||||
provider: "manual",
|
||||
})
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(authIdentities).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-id-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const id = "test-id"
|
||||
|
||||
it("should return an authIdentity for the given id", async () => {
|
||||
const authIdentity = await service.retrieve(id)
|
||||
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return authIdentity based on config select param", async () => {
|
||||
const authIdentity = await service.retrieve(id, {
|
||||
select: ["id"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentity))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw an error when an authIdentity with the given id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"AuthIdentity with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a authIdentityId is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("authIdentity - id must be defined")
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete the authIdentities given an id successfully", async () => {
|
||||
const id = "test-id"
|
||||
|
||||
await service.delete([id])
|
||||
|
||||
const authIdentities = await service.list({
|
||||
id: [id],
|
||||
})
|
||||
|
||||
expect(authIdentities).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("should throw an error when a id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'AuthIdentity with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
|
||||
it("should update authIdentity", async () => {
|
||||
const id = "test-id"
|
||||
|
||||
await service.update([
|
||||
{
|
||||
id,
|
||||
provider_metadata: { email: "test@email.com" },
|
||||
},
|
||||
])
|
||||
|
||||
const [authIdentity] = await service.list({ id: [id] })
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
provider_metadata: { email: "test@email.com" },
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a authIdentity successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
id: "test",
|
||||
provider: "manual",
|
||||
entity_id: "test",
|
||||
scope: "store",
|
||||
},
|
||||
])
|
||||
|
||||
const [authIdentity] = await service.list({
|
||||
id: ["test"],
|
||||
})
|
||||
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthModuleService - AuthProvider", () => {
|
||||
describe("authenticate", () => {
|
||||
it("authenticate validates that a provider is registered in container", async () => {
|
||||
const { success, error } = await service.authenticate(
|
||||
"notRegistered",
|
||||
{} as any
|
||||
)
|
||||
|
||||
expect(success).toBe(false)
|
||||
expect(error).toEqual(
|
||||
"AuthenticationProvider: notRegistered wasn't registered in the module. Have you configured your options correctly?"
|
||||
)
|
||||
})
|
||||
|
||||
it("fails to authenticate using a valid provider with an invalid scope", async () => {
|
||||
const { success, error } = await service.authenticate("emailpass", {
|
||||
authScope: "non-existing",
|
||||
} as any)
|
||||
|
||||
expect(success).toBe(false)
|
||||
expect(error).toEqual(
|
||||
`Scope "non-existing" is not valid for provider emailpass`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,133 +0,0 @@
|
||||
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
const seedDefaultData = async (manager) => {
|
||||
await createAuthIdentities(manager)
|
||||
}
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
moduleOptions: {
|
||||
providers: [
|
||||
{
|
||||
name: "emailpass",
|
||||
scopes: {
|
||||
admin: {},
|
||||
store: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthModuleService - AuthProvider", () => {
|
||||
describe("authenticate", () => {
|
||||
it("authenticate validates that a provider is registered in container", async () => {
|
||||
const password = "supersecret"
|
||||
const email = "test@test.com"
|
||||
const passwordHash = (
|
||||
await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 })
|
||||
).toString("base64")
|
||||
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager(), [
|
||||
// Add authenticated user
|
||||
{
|
||||
provider: "emailpass",
|
||||
entity_id: email,
|
||||
scope: "store",
|
||||
provider_metadata: {
|
||||
password: passwordHash,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: {
|
||||
email: "test@test.com",
|
||||
password: password,
|
||||
},
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: true,
|
||||
authIdentity: expect.objectContaining({
|
||||
entity_id: email,
|
||||
provider_metadata: {},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it("fails when no password is given", async () => {
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: { email: "test@test.com" },
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
error: "Password should be a string",
|
||||
})
|
||||
})
|
||||
|
||||
it("fails when no email is given", async () => {
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: { password: "supersecret" },
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
error: "Email should be a string",
|
||||
})
|
||||
})
|
||||
|
||||
it("fails with an invalid password", async () => {
|
||||
const password = "supersecret"
|
||||
const email = "test@test.com"
|
||||
const passwordHash = (
|
||||
await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 })
|
||||
).toString("base64")
|
||||
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager(), [
|
||||
// Add authenticated user
|
||||
{
|
||||
provider: "emailpass",
|
||||
scope: "store",
|
||||
entity_id: email,
|
||||
provider_metadata: {
|
||||
password_hash: passwordHash,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: {
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
},
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -48,6 +48,7 @@
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/auth-emailpass": "0.0.1",
|
||||
"@medusajs/modules-sdk": "^1.12.9",
|
||||
"@medusajs/types": "^1.11.14",
|
||||
"@medusajs/utils": "^1.11.7",
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import * as defaultProviders from "@providers"
|
||||
import EmailPassProvider from "@medusajs/auth-emailpass"
|
||||
|
||||
import { LoaderOptions, ModulesSdkTypes, ModuleProvider } from "@medusajs/types"
|
||||
import { Lifetime, asFunction, asValue } from "awilix"
|
||||
import { moduleProviderLoader } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
AuthModuleProviderConfig,
|
||||
AuthProviderScope,
|
||||
LoaderOptions,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
AwilixContainer,
|
||||
ClassOrFunctionReturning,
|
||||
Constructor,
|
||||
Resolver,
|
||||
asClass,
|
||||
} from "awilix"
|
||||
AuthIdentifiersRegistrationName,
|
||||
AuthProviderRegistrationPrefix,
|
||||
} from "@types"
|
||||
|
||||
type AuthModuleProviders = {
|
||||
providers: AuthModuleProviderConfig[]
|
||||
const registrationFn = async (klass, container, pluginOptions) => {
|
||||
Object.entries(pluginOptions.config || []).map(([name, config]) => {
|
||||
container.register({
|
||||
[AuthProviderRegistrationPrefix + name]: asFunction(
|
||||
(cradle) => new klass(cradle, config),
|
||||
{
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
container.registerAdd(AuthIdentifiersRegistrationName, asValue(name))
|
||||
})
|
||||
}
|
||||
|
||||
export default async ({
|
||||
@@ -25,48 +30,28 @@ export default async ({
|
||||
(
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
) &
|
||||
AuthModuleProviders
|
||||
) & { providers: ModuleProvider[] }
|
||||
>): Promise<void> => {
|
||||
const providerMap = new Map(
|
||||
options?.providers?.map((provider) => [provider.name, provider.scopes]) ??
|
||||
[]
|
||||
)
|
||||
// TODO: Temporary settings used by the starter, remove once the auth module is updated
|
||||
const isLegacyOptions =
|
||||
options?.providers?.length && !!(options?.providers[0] as any)?.name
|
||||
|
||||
// if(options?.providers?.length) {
|
||||
// TODO: implement plugin provider registration
|
||||
// }
|
||||
// Note: For now we want to inject some providers out of the box
|
||||
const providerConfig = [
|
||||
{
|
||||
resolve: EmailPassProvider,
|
||||
options: {
|
||||
config: {
|
||||
emailpass: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(isLegacyOptions ? [] : options?.providers ?? []),
|
||||
]
|
||||
|
||||
const providersToLoad = Object.values(defaultProviders)
|
||||
|
||||
for (const provider of providersToLoad) {
|
||||
container.register({
|
||||
[`auth_provider_${provider.PROVIDER}`]: asClass(
|
||||
provider as Constructor<any>
|
||||
)
|
||||
.singleton()
|
||||
.inject(() => ({ scopes: providerMap.get(provider.PROVIDER) ?? {} })),
|
||||
})
|
||||
}
|
||||
|
||||
container.register({
|
||||
[`auth_providers`]: asArray(providersToLoad, providerMap),
|
||||
await moduleProviderLoader({
|
||||
container,
|
||||
providers: providerConfig,
|
||||
registerServiceFn: registrationFn,
|
||||
})
|
||||
}
|
||||
|
||||
function asArray(
|
||||
resolvers: (ClassOrFunctionReturning<unknown> | Resolver<unknown>)[],
|
||||
providerScopeMap: Map<string, Record<string, AuthProviderScope>>
|
||||
): { resolve: (container: AwilixContainer) => unknown[] } {
|
||||
return {
|
||||
resolve: (container: AwilixContainer) =>
|
||||
resolvers.map((resolver) =>
|
||||
asClass(resolver as Constructor<any>)
|
||||
.inject(() => ({
|
||||
// @ts-ignore
|
||||
scopes: providerScopeMap.get(resolver.PROVIDER) ?? {},
|
||||
}))
|
||||
.resolve(container)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,6 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"user_metadata": {
|
||||
"name": "user_metadata",
|
||||
"type": "jsonb",
|
||||
@@ -72,8 +63,8 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_auth_identity_provider_scope_entity_id",
|
||||
"columnNames": ["provider", "scope", "entity_id"],
|
||||
"keyName": "IDX_auth_identity_provider_entity_id",
|
||||
"columnNames": ["provider", "entity_id"],
|
||||
"composite": true,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240205025924 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table if not exists "auth_identity" ("id" text not null, "entity_id" text not null, "provider" text not null, "scope" text not null, "user_metadata" jsonb null, "app_metadata" jsonb not null, "provider_metadata" jsonb null, constraint "auth_identity_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "auth_identity" add constraint "IDX_auth_identity_provider_scope_entity_id" unique ("provider", "scope", "entity_id");'
|
||||
)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "auth_identity" cascade;')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240205025928 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table if not exists "auth_identity" ("id" text not null, "entity_id" text not null, "provider" text not null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_identity_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "auth_identity" add constraint "IDX_auth_identity_provider_entity_id" unique ("provider", "entity_id");'
|
||||
)
|
||||
|
||||
this.addSql('alter table "auth_identity" drop column if exists "scope";')
|
||||
this.addSql(
|
||||
`alter table "auth_identity" alter column "app_metadata" drop not null;`
|
||||
)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "auth_identity" cascade;')
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata"
|
||||
|
||||
@Entity()
|
||||
@Unique({
|
||||
properties: ["provider", "scope", "entity_id"],
|
||||
name: "IDX_auth_identity_provider_scope_entity_id",
|
||||
properties: ["provider", "entity_id"],
|
||||
name: "IDX_auth_identity_provider_entity_id",
|
||||
})
|
||||
export default class AuthIdentity {
|
||||
[OptionalProps]: OptionalFields
|
||||
@@ -29,14 +29,11 @@ export default class AuthIdentity {
|
||||
@Property({ columnType: "text" })
|
||||
provider: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
scope: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
user_metadata: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "jsonb" })
|
||||
app_metadata: Record<string, unknown> = {}
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
app_metadata: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
provider_metadata: Record<string, unknown> | null = null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthModuleService } from "@services"
|
||||
import { ModuleExports } from "@medusajs/types"
|
||||
import { AuthModuleService } from "@services"
|
||||
import loadProviders from "./loaders/providers"
|
||||
|
||||
const service = AuthModuleService
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
|
||||
import {
|
||||
AbstractAuthModuleProvider,
|
||||
MedusaError,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { AuthIdentityService } from "@services"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
|
||||
class EmailPasswordProvider extends AbstractAuthModuleProvider {
|
||||
public static PROVIDER = "emailpass"
|
||||
public static DISPLAY_NAME = "Email/Password Authentication"
|
||||
|
||||
protected readonly authIdentitySerivce_: AuthIdentityService
|
||||
|
||||
constructor({
|
||||
authIdentityService,
|
||||
}: {
|
||||
authIdentityService: AuthIdentityService
|
||||
}) {
|
||||
super(arguments[0], {
|
||||
provider: EmailPasswordProvider.PROVIDER,
|
||||
displayName: EmailPasswordProvider.DISPLAY_NAME,
|
||||
})
|
||||
|
||||
this.authIdentitySerivce_ = authIdentityService
|
||||
}
|
||||
|
||||
private getHashConfig() {
|
||||
const scopeConfig = this.scopeConfig_.hashConfig as
|
||||
| Scrypt.ScryptParams
|
||||
| undefined
|
||||
|
||||
const defaultHashConfig = { logN: 15, r: 8, p: 1 }
|
||||
|
||||
// Return custom defined hash config or default hash parameters
|
||||
return scopeConfig ?? defaultHashConfig
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
userData: AuthenticationInput
|
||||
): 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",
|
||||
}
|
||||
}
|
||||
let authIdentity
|
||||
|
||||
try {
|
||||
authIdentity =
|
||||
await this.authIdentitySerivce_.retrieveByProviderAndEntityId(
|
||||
email,
|
||||
EmailPasswordProvider.PROVIDER
|
||||
)
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
const password_hash = await Scrypt.kdf(password, this.getHashConfig())
|
||||
|
||||
const [createdAuthIdentity] = await this.authIdentitySerivce_.create([
|
||||
{
|
||||
entity_id: email,
|
||||
provider: EmailPasswordProvider.PROVIDER,
|
||||
scope: this.scope_,
|
||||
provider_metadata: {
|
||||
password: password_hash.toString("base64"),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authIdentity: JSON.parse(JSON.stringify(createdAuthIdentity)),
|
||||
}
|
||||
}
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
const password_hash = authIdentity.provider_metadata?.password
|
||||
|
||||
if (isString(password_hash)) {
|
||||
const buf = Buffer.from(password_hash as string, "base64")
|
||||
|
||||
const success = await Scrypt.verify(buf, password)
|
||||
|
||||
if (success) {
|
||||
delete authIdentity.provider_metadata!.password
|
||||
|
||||
return {
|
||||
success,
|
||||
authIdentity: JSON.parse(JSON.stringify(authIdentity)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailPasswordProvider
|
||||
@@ -1,13 +1,12 @@
|
||||
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
import { AuthIdentityService } from "@services"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
|
||||
import { AuthorizationCode } from "simple-oauth2"
|
||||
import url from "url"
|
||||
|
||||
type InjectedDependencies = {
|
||||
authIdentityService: AuthIdentityService
|
||||
authIdentityService: any
|
||||
}
|
||||
|
||||
type ProviderConfig = {
|
||||
@@ -18,15 +17,12 @@ type ProviderConfig = {
|
||||
}
|
||||
|
||||
class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
public static PROVIDER = "google"
|
||||
public static DISPLAY_NAME = "Google Authentication"
|
||||
protected readonly authIdentityService_: any
|
||||
|
||||
protected readonly authIdentityService_: AuthIdentityService
|
||||
|
||||
constructor({ authIdentityService }: InjectedDependencies) {
|
||||
constructor({ authIdentityService }: InjectedDependencies, options: any) {
|
||||
super(arguments[0], {
|
||||
provider: GoogleProvider.PROVIDER,
|
||||
displayName: GoogleProvider.DISPLAY_NAME,
|
||||
provider: "google",
|
||||
displayName: "Google Authentication",
|
||||
})
|
||||
|
||||
this.authIdentityService_ = authIdentityService
|
||||
@@ -72,6 +68,9 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
}
|
||||
|
||||
const code = req.query?.code ?? req.body?.code
|
||||
if (!code) {
|
||||
return { success: false, error: "No code provided" }
|
||||
}
|
||||
|
||||
return await this.validateCallbackToken(code, config)
|
||||
}
|
||||
@@ -89,16 +88,15 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
authIdentity =
|
||||
await this.authIdentityService_.retrieveByProviderAndEntityId(
|
||||
entity_id,
|
||||
GoogleProvider.PROVIDER
|
||||
this.provider
|
||||
)
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
const [createdAuthIdentity] = await this.authIdentityService_.create([
|
||||
{
|
||||
entity_id,
|
||||
provider: GoogleProvider.PROVIDER,
|
||||
provider: this.provider,
|
||||
user_metadata: jwtData!.payload,
|
||||
scope: this.scope_,
|
||||
},
|
||||
])
|
||||
authIdentity = createdAuthIdentity
|
||||
@@ -132,7 +130,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
accessToken.token.id_token
|
||||
)
|
||||
|
||||
const { successRedirectUrl } = this.getConfigFromScope()
|
||||
const { successRedirectUrl } = this.getConfig()
|
||||
|
||||
return {
|
||||
success,
|
||||
@@ -144,8 +142,10 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private getConfigFromScope(): ProviderConfig {
|
||||
const config: Partial<ProviderConfig> = { ...this.scopeConfig_ }
|
||||
private getConfig(): ProviderConfig {
|
||||
// TODO: Fetch this from provider config
|
||||
// const config: Partial<ProviderConfig> = { ...this.scopeConfig_ }
|
||||
const config = {} as any
|
||||
|
||||
if (!config.clientID) {
|
||||
throw new Error("Google clientID is required")
|
||||
@@ -163,7 +163,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
}
|
||||
|
||||
private originalURL(req: AuthenticationInput) {
|
||||
const host = req.headers.host
|
||||
const host = req.headers?.host
|
||||
const protocol = req.protocol
|
||||
const path = req.url || ""
|
||||
|
||||
@@ -173,7 +173,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
private async getProviderConfig(
|
||||
req: AuthenticationInput
|
||||
): Promise<ProviderConfig> {
|
||||
const config = this.getConfigFromScope()
|
||||
const config = this.getConfig()
|
||||
|
||||
const callbackURL = config.callbackURL
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as EmailPasswordProvider } from "./email-password"
|
||||
export { default as GoogleProvider } from "./google"
|
||||
export { default as GoogleProvider } from "./google"
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
AuthTypes,
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
RepositoryService,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { AuthIdentity } from "@models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
authIdentityRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class AuthIdentityService<
|
||||
TEntity extends AuthIdentity = AuthIdentity
|
||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
||||
AuthIdentity
|
||||
)<TEntity> {
|
||||
protected readonly authIdentityRepository_: RepositoryService<TEntity>
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.authIdentityRepository_ = container.authIdentityRepository
|
||||
this.baseRepository_ = container.baseRepository
|
||||
}
|
||||
|
||||
@InjectManager("authIdentityRepository_")
|
||||
async retrieveByProviderAndEntityId<
|
||||
TEntityMethod = AuthTypes.AuthIdentityDTO
|
||||
>(
|
||||
entityId: string,
|
||||
provider: string,
|
||||
config: FindConfig<TEntityMethod> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<AuthTypes.AuthIdentityDTO> {
|
||||
const queryConfig = ModulesSdkUtils.buildQuery<TEntity>(
|
||||
{ entity_id: entityId, provider },
|
||||
{ ...config, take: 1 }
|
||||
)
|
||||
const [result] = await this.authIdentityRepository_.find(
|
||||
queryConfig,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`AuthIdentity with entity_id: "${entityId}" and provider: "${provider}" not found`
|
||||
)
|
||||
}
|
||||
|
||||
return await this.baseRepository_.serialize<AuthTypes.AuthIdentityDTO>(
|
||||
result
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
AuthIdentityProviderService,
|
||||
AuthTypes,
|
||||
Context,
|
||||
DAL,
|
||||
@@ -14,16 +15,17 @@ import { AuthIdentity } from "@models"
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
|
||||
import {
|
||||
AbstractAuthModuleProvider,
|
||||
InjectManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import AuthProviderService from "./auth-provider"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
authIdentityService: ModulesSdkTypes.InternalModuleService<any>
|
||||
authProviderService: AuthProviderService
|
||||
}
|
||||
|
||||
const generateMethodForModels = [AuthIdentity]
|
||||
@@ -42,9 +44,14 @@ export default class AuthModuleService<
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected authIdentityService_: ModulesSdkTypes.InternalModuleService<TAuthIdentity>
|
||||
protected readonly authProviderService_: AuthProviderService
|
||||
|
||||
constructor(
|
||||
{ authIdentityService, baseRepository }: InjectedDependencies,
|
||||
{
|
||||
authIdentityService,
|
||||
authProviderService,
|
||||
baseRepository,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
@@ -52,6 +59,7 @@ export default class AuthModuleService<
|
||||
|
||||
this.baseRepository_ = baseRepository
|
||||
this.authIdentityService_ = authIdentityService
|
||||
this.authProviderService_ = authProviderService
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
@@ -116,34 +124,16 @@ export default class AuthModuleService<
|
||||
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
|
||||
}
|
||||
|
||||
protected getRegisteredAuthenticationProvider(
|
||||
provider: string,
|
||||
{ authScope }: AuthenticationInput
|
||||
): AbstractAuthModuleProvider {
|
||||
let containerProvider: AbstractAuthModuleProvider
|
||||
try {
|
||||
containerProvider = this.__container__[`auth_provider_${provider}`]
|
||||
} catch (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`AuthenticationProvider: ${provider} wasn't registered in the module. Have you configured your options correctly?`
|
||||
)
|
||||
}
|
||||
|
||||
return containerProvider.withScope(authScope)
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
provider: string,
|
||||
authenticationData: AuthenticationInput
|
||||
): Promise<AuthenticationResponse> {
|
||||
try {
|
||||
const registeredProvider = this.getRegisteredAuthenticationProvider(
|
||||
return await this.authProviderService_.authenticate(
|
||||
provider,
|
||||
authenticationData
|
||||
authenticationData,
|
||||
this.getAuthIdentityProviderService()
|
||||
)
|
||||
|
||||
return await registeredProvider.authenticate(authenticationData)
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
@@ -154,14 +144,49 @@ export default class AuthModuleService<
|
||||
authenticationData: AuthenticationInput
|
||||
): Promise<AuthenticationResponse> {
|
||||
try {
|
||||
const registeredProvider = this.getRegisteredAuthenticationProvider(
|
||||
return await this.authProviderService_.validateCallback(
|
||||
provider,
|
||||
authenticationData
|
||||
authenticationData,
|
||||
this.getAuthIdentityProviderService()
|
||||
)
|
||||
|
||||
return await registeredProvider.validateCallback(authenticationData)
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
getAuthIdentityProviderService(): AuthIdentityProviderService {
|
||||
return {
|
||||
retrieve: async ({ entity_id, provider }) => {
|
||||
const authIdentities = await this.authIdentityService_.list({
|
||||
entity_id,
|
||||
provider,
|
||||
})
|
||||
|
||||
if (!authIdentities.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`AuthIdentity with entity_id "${entity_id}" not found`
|
||||
)
|
||||
}
|
||||
|
||||
if (authIdentities.length > 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Multiple authIdentities found for entity_id "${entity_id}"`
|
||||
)
|
||||
}
|
||||
|
||||
return await this.baseRepository_.serialize<AuthTypes.AuthIdentityDTO>(
|
||||
authIdentities[0]
|
||||
)
|
||||
},
|
||||
create: async (data: AuthTypes.CreateAuthIdentityDTO) => {
|
||||
const createdAuthIdentity = await this.authIdentityService_.create(data)
|
||||
|
||||
return await this.baseRepository_.serialize<AuthTypes.AuthIdentityDTO>(
|
||||
createdAuthIdentity
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
packages/modules/auth/src/services/auth-provider.ts
Normal file
56
packages/modules/auth/src/services/auth-provider.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
AuthTypes,
|
||||
AuthenticationInput,
|
||||
AuthIdentityProviderService,
|
||||
AuthenticationResponse,
|
||||
} from "@medusajs/types"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { AuthProviderRegistrationPrefix } from "@types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
[
|
||||
key: `${typeof AuthProviderRegistrationPrefix}${string}`
|
||||
]: AuthTypes.IAuthProvider
|
||||
}
|
||||
|
||||
export default class AuthProviderService {
|
||||
protected dependencies: InjectedDependencies
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
this.dependencies = container
|
||||
}
|
||||
|
||||
protected retrieveProviderRegistration(
|
||||
providerId: string
|
||||
): AuthTypes.IAuthProvider {
|
||||
try {
|
||||
return this.dependencies[`${AuthProviderRegistrationPrefix}${providerId}`]
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a auth provider with id: ${providerId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
provider: string,
|
||||
auth: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
const providerHandler = this.retrieveProviderRegistration(provider)
|
||||
return await providerHandler.authenticate(auth, authIdentityProviderService)
|
||||
}
|
||||
|
||||
async validateCallback(
|
||||
provider: string,
|
||||
auth: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
const providerHandler = this.retrieveProviderRegistration(provider)
|
||||
return await providerHandler.validateCallback(
|
||||
auth,
|
||||
authIdentityProviderService
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as AuthModuleService } from "./auth-module"
|
||||
export { default as AuthIdentityService } from "./auth-identity"
|
||||
export { default as AuthProviderService } from "./auth-provider"
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import { ModuleProviderExports } from "@medusajs/types"
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
import { Logger } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
}
|
||||
|
||||
export const AuthIdentifiersRegistrationName = "auth_providers_identifier"
|
||||
|
||||
export const AuthProviderRegistrationPrefix = "au_"
|
||||
|
||||
export type AuthModuleOptions = Partial<ModuleServiceInitializeOptions> & {
|
||||
/**
|
||||
* Providers to be registered
|
||||
*/
|
||||
providers?: {
|
||||
/**
|
||||
* The module provider to be registered
|
||||
*/
|
||||
resolve: string | ModuleProviderExports
|
||||
options: {
|
||||
/**
|
||||
* key value pair of the provider name and the configuration to be passed to the provider constructor
|
||||
*/
|
||||
config: Record<string, unknown>
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
0
packages/modules/providers/auth-emailpass/.gitignore
vendored
Normal file
0
packages/modules/providers/auth-emailpass/.gitignore
vendored
Normal file
0
packages/modules/providers/auth-emailpass/README.md
Normal file
0
packages/modules/providers/auth-emailpass/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
import { EmailPassAuthService } from "../../src/services/emailpass"
|
||||
jest.setTimeout(100000)
|
||||
|
||||
describe("Email password auth provider", () => {
|
||||
let emailpassService: EmailPassAuthService
|
||||
|
||||
beforeAll(() => {
|
||||
emailpassService = new EmailPassAuthService(
|
||||
{
|
||||
logger: console as any,
|
||||
},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("return error if email is not passed", async () => {
|
||||
const resp = await emailpassService.authenticate(
|
||||
{ body: { password: "otherpass" } },
|
||||
{}
|
||||
)
|
||||
|
||||
expect(resp).toEqual({
|
||||
error: "Email should be a string",
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("return error if password is not passed", async () => {
|
||||
const resp = await emailpassService.authenticate(
|
||||
{ body: { email: "test@admin.com" } },
|
||||
{}
|
||||
)
|
||||
|
||||
expect(resp).toEqual({
|
||||
error: "Password should be a string",
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("return error if the passwords don't match", async () => {
|
||||
const config = { logN: 15, r: 8, p: 1 }
|
||||
const passwordHash = await Scrypt.kdf("somepass", config)
|
||||
|
||||
const authServiceSpies = {
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
entity_id: "test@admin.com",
|
||||
provider: "emailpass",
|
||||
provider_metadata: {
|
||||
password: passwordHash.toString("base64"),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const resp = await emailpassService.authenticate(
|
||||
{ body: { email: "test@admin.com", password: "otherpass" } },
|
||||
authServiceSpies
|
||||
)
|
||||
|
||||
expect(authServiceSpies.retrieve).toHaveBeenCalled()
|
||||
expect(resp).toEqual({
|
||||
error: "Invalid email or password",
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("return an existing entity if the passwords match", async () => {
|
||||
const config = { logN: 15, r: 8, p: 1 }
|
||||
const passwordHash = await Scrypt.kdf("somepass", config)
|
||||
|
||||
const authServiceSpies = {
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
entity_id: "test@admin.com",
|
||||
provider: "emailpass",
|
||||
provider_metadata: {
|
||||
password: passwordHash.toString("base64"),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const resp = await emailpassService.authenticate(
|
||||
{ body: { email: "test@admin.com", password: "somepass" } },
|
||||
authServiceSpies
|
||||
)
|
||||
|
||||
expect(authServiceSpies.retrieve).toHaveBeenCalled()
|
||||
expect(resp).toEqual(
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
authIdentity: expect.objectContaining({
|
||||
entity_id: "test@admin.com",
|
||||
provider_metadata: {},
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a new auth identity if it doesn't exist", async () => {
|
||||
const authServiceSpies = {
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Not found")
|
||||
}),
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
entity_id: "test@admin.com",
|
||||
provider: "emailpass",
|
||||
provider_metadata: {
|
||||
password: "somehash",
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const resp = await emailpassService.authenticate(
|
||||
{ body: { email: "test@admin.com", password: "test" } },
|
||||
authServiceSpies
|
||||
)
|
||||
|
||||
expect(authServiceSpies.retrieve).toHaveBeenCalled()
|
||||
expect(authServiceSpies.create).toHaveBeenCalled()
|
||||
|
||||
expect(resp.authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
entity_id: "test@admin.com",
|
||||
provider_metadata: {},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
7
packages/modules/providers/auth-emailpass/jest.config.js
Normal file
7
packages/modules/providers/auth-emailpass/jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "@swc/jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
}
|
||||
40
packages/modules/providers/auth-emailpass/package.json
Normal file
40
packages/modules/providers/auth-emailpass/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@medusajs/auth-emailpass",
|
||||
"version": "0.0.1",
|
||||
"description": "Email and password credential authentication provider for Medusa",
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/modules/providers/auth-emailpass"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
|
||||
"test": "jest --passWithNoTests src",
|
||||
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
|
||||
"build": "rimraf dist && tsc -p ./tsconfig.json",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"rimraf": "^5.0.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/utils": "^1.11.7",
|
||||
"scrypt-kdf": "^2.0.1"
|
||||
},
|
||||
"keywords": [
|
||||
"medusa-provider",
|
||||
"medusa-provider-auth-userpass"
|
||||
]
|
||||
}
|
||||
10
packages/modules/providers/auth-emailpass/src/index.ts
Normal file
10
packages/modules/providers/auth-emailpass/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ModuleProviderExports } from "@medusajs/types"
|
||||
import { EmailPassAuthService } from "./services/emailpass"
|
||||
|
||||
const services = [EmailPassAuthService]
|
||||
|
||||
const providerExport: ModuleProviderExports = {
|
||||
services,
|
||||
}
|
||||
|
||||
export default providerExport
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Logger,
|
||||
EmailPassAuthProviderOptions,
|
||||
AuthenticationResponse,
|
||||
AuthenticationInput,
|
||||
AuthIdentityProviderService,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
AbstractAuthModuleProvider,
|
||||
MedusaError,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
interface LocalServiceConfig extends EmailPassAuthProviderOptions {}
|
||||
|
||||
export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
||||
protected config_: LocalServiceConfig
|
||||
protected logger_: Logger
|
||||
|
||||
constructor(
|
||||
{ logger }: InjectedDependencies,
|
||||
options: EmailPassAuthProviderOptions
|
||||
) {
|
||||
super(
|
||||
{},
|
||||
{ provider: "emailpass", displayName: "Email/Password Authentication" }
|
||||
)
|
||||
this.config_ = options
|
||||
this.logger_ = logger
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
userData: AuthenticationInput,
|
||||
authIdentityService: AuthIdentityProviderService
|
||||
): 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",
|
||||
}
|
||||
}
|
||||
let authIdentity
|
||||
|
||||
try {
|
||||
authIdentity = await authIdentityService.retrieve({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
const config = this.config_.hashConfig ?? { logN: 15, r: 8, p: 1 }
|
||||
const passwordHash = await Scrypt.kdf(password, config)
|
||||
|
||||
const createdAuthIdentity = await authIdentityService.create({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
provider_metadata: {
|
||||
password: passwordHash.toString("base64"),
|
||||
},
|
||||
})
|
||||
|
||||
const copy = JSON.parse(JSON.stringify(createdAuthIdentity))
|
||||
delete copy.provider_metadata?.password
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authIdentity: copy,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
const passwordHash = authIdentity.provider_metadata?.password
|
||||
|
||||
if (isString(passwordHash)) {
|
||||
const buf = Buffer.from(passwordHash as string, "base64")
|
||||
const success = await Scrypt.verify(buf, password)
|
||||
|
||||
if (success) {
|
||||
const copy = JSON.parse(JSON.stringify(authIdentity))
|
||||
delete copy.provider_metadata!.password
|
||||
|
||||
return {
|
||||
success,
|
||||
authIdentity: copy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
}
|
||||
32
packages/modules/providers/auth-emailpass/tsconfig.json
Normal file
32
packages/modules/providers/auth-emailpass/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2021"],
|
||||
"target": "es2021",
|
||||
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true, // to use ES5 specific tooling
|
||||
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules",
|
||||
".eslintrc.js"
|
||||
]
|
||||
}
|
||||
14
yarn.lock
14
yarn.lock
@@ -5092,10 +5092,24 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/auth-emailpass@0.0.1, @medusajs/auth-emailpass@workspace:packages/modules/providers/auth-emailpass":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/auth-emailpass@workspace:packages/modules/providers/auth-emailpass"
|
||||
dependencies:
|
||||
"@medusajs/utils": ^1.11.7
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
rimraf: ^5.0.1
|
||||
scrypt-kdf: ^2.0.1
|
||||
typescript: ^4.9.5
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/auth@workspace:*, @medusajs/auth@workspace:packages/modules/auth":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/auth@workspace:packages/modules/auth"
|
||||
dependencies:
|
||||
"@medusajs/auth-emailpass": 0.0.1
|
||||
"@medusajs/modules-sdk": ^1.12.9
|
||||
"@medusajs/types": ^1.11.14
|
||||
"@medusajs/utils": ^1.11.7
|
||||
|
||||
Reference in New Issue
Block a user