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:
Stevche Radevski
2024-05-23 20:56:40 +02:00
committed by GitHub
parent 7b0cfe3b77
commit 8a070d5d85
100 changed files with 991 additions and 1005 deletions
@@ -33,7 +33,7 @@ medusaIntegrationTestRunner({
}) })
}, },
async () => { async () => {
return await api.post("/auth/admin/emailpass", { return await api.post("/auth/user/emailpass", {
email: "admin@medusa.js", email: "admin@medusa.js",
password: "secret_password", password: "secret_password",
}) })
@@ -63,7 +63,7 @@ medusaIntegrationTestRunner({
it("should test the entire authentication lifecycle", async () => { it("should test the entire authentication lifecycle", async () => {
// sign in // sign in
const response = await api.post("/auth/admin/emailpass", { const response = await api.post("/auth/user/emailpass", {
email: "admin@medusa.js", email: "admin@medusa.js",
password: "secret_password", password: "secret_password",
}) })
@@ -175,7 +175,7 @@ medusaIntegrationTestRunner({
token = await breaking( token = await breaking(
() => null, () => null,
async () => { async () => {
const emailPassResponse = await api.post("/auth/admin/emailpass", { const emailPassResponse = await api.post("/auth/user/emailpass", {
email: "test@test123.com", email: "test@test123.com",
password: "test123", password: "test123",
}) })
@@ -230,7 +230,7 @@ medusaIntegrationTestRunner({
// V2 only test // V2 only test
it.skip("should throw, if session/bearer auth is present for existing user", async () => { 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", email: "test@test123.com",
password: "test123", password: "test123",
}) })
+1 -16
View File
@@ -41,22 +41,7 @@ module.exports = {
options: { ttl: cacheTTL }, options: { ttl: cacheTTL },
}, },
workflows: true, workflows: true,
[Modules.AUTH]: { [Modules.AUTH]: true,
scope: "internal",
resources: "shared",
resolve: "@medusajs/auth",
options: {
providers: [
{
name: "emailpass",
scopes: {
admin: {},
store: {},
},
},
],
},
},
[Modules.USER]: { [Modules.USER]: {
scope: "internal", scope: "internal",
resources: "shared", resources: "shared",
@@ -32,7 +32,6 @@ export const createAdminUser = async (
const authIdentity = await authModule.create({ const authIdentity = await authModule.create({
provider: "emailpass", provider: "emailpass",
entity_id: "admin@medusa.js", entity_id: "admin@medusa.js",
scope: "admin",
provider_metadata: { provider_metadata: {
password: "somepassword", password: "somepassword",
}, },
@@ -55,8 +54,6 @@ export const createAdminUser = async (
actor_id: user.id, actor_id: user.id,
actor_type: "user", actor_type: "user",
auth_identity_id: authIdentity.id, auth_identity_id: authIdentity.id,
scope: "admin",
app_metadata: {},
}, },
"test" "test"
) )
@@ -43,14 +43,13 @@ medusaIntegrationTestRunner({
await authService.create({ await authService.create({
provider: "emailpass", provider: "emailpass",
entity_id: email, entity_id: email,
scope: "admin",
provider_metadata: { provider_metadata: {
password: passwordHash, password: passwordHash,
}, },
}) })
const response = await api const response = await api
.post(`/auth/admin/emailpass`, { .post(`/auth/user/emailpass`, {
email: email, email: email,
password: password, password: password,
}) })
@@ -75,14 +74,13 @@ medusaIntegrationTestRunner({
await authService.create({ await authService.create({
provider: "emailpass", provider: "emailpass",
entity_id: email, entity_id: email,
scope: "admin",
provider_metadata: { provider_metadata: {
password: passwordHash, password: passwordHash,
}, },
}) })
const error = await api const error = await api
.post(`/auth/admin/emailpass`, { .post(`/auth/user/emailpass`, {
email: email, email: email,
password: "incorrect-password", password: "incorrect-password",
}) })
@@ -101,7 +99,7 @@ medusaIntegrationTestRunner({
).toString("base64") ).toString("base64")
const error = await api const error = await api
.post(`/auth/admin/emailpass`, { .post(`/auth/user/emailpass`, {
email: "should-not-exist", email: "should-not-exist",
password: "should-not-exist", password: "should-not-exist",
}) })
@@ -38,7 +38,6 @@ medusaIntegrationTestRunner({
const authIdentity = await authService.create({ const authIdentity = await authService.create({
entity_id: "store_user", entity_id: "store_user",
provider: "emailpass", provider: "emailpass",
scope: "store",
}) })
const token = jwt.sign(authIdentity, http.jwtSecret) 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 () => { 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", email: "potential_member@test.com",
password: "supersecret", password: "supersecret",
}) })
@@ -58,7 +58,7 @@ medusaIntegrationTestRunner({
email: "potential_member@test.com", 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", email: "potential_member@test.com",
password: "supersecret", password: "supersecret",
}) })
@@ -92,7 +92,7 @@ medusaIntegrationTestRunner({
email: "potential_member@test.com", 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", email: "some-email@test.com",
password: "supersecret", password: "supersecret",
}) })
@@ -25,7 +25,6 @@ export const createAuthenticatedCustomer = async (
const authIdentity = await authService.create({ const authIdentity = await authService.create({
entity_id: "store_user", entity_id: "store_user",
provider: "emailpass", provider: "emailpass",
scope: "store",
}) })
// Ideally we simulate a signup process than manually linking here. // Ideally we simulate a signup process than manually linking here.
@@ -45,8 +44,6 @@ export const createAuthenticatedCustomer = async (
actor_id: customer.id, actor_id: customer.id,
actor_type: "customer", actor_type: "customer",
auth_identity_id: authIdentity.id, auth_identity_id: authIdentity.id,
scope: "store",
app_metadata: {},
}, },
http.jwtSecret http.jwtSecret
) )
+1 -16
View File
@@ -47,22 +47,7 @@ module.exports = {
medusa_v2: enableMedusaV2, medusa_v2: enableMedusaV2,
}, },
modules: { modules: {
[Modules.AUTH]: { [Modules.AUTH]: true,
scope: "internal",
resources: "shared",
resolve: "@medusajs/auth",
options: {
providers: [
{
name: "emailpass",
scopes: {
admin: {},
store: {},
},
},
],
},
},
[Modules.USER]: { [Modules.USER]: {
scope: "internal", scope: "internal",
resources: "shared", resources: "shared",
+1 -1
View File
@@ -5,7 +5,7 @@ status_code=$(curl \
-H "Content-Type: application/json"\ -H "Content-Type: application/json"\
-d '{"email":"'$1'", "password":"'$2'"}'\ -d '{"email":"'$1'", "password":"'$2'"}'\
--write-out %{http_code}\ --write-out %{http_code}\
http://localhost:9000/auth/admin/emailpass) http://localhost:9000/auth/user/emailpass)
if [[ "$status_code" -ne 200 ]] ; then if [[ "$status_code" -ne 200 ]] ; then
echo "Site status changed to $status_code" echo "Site status changed to $status_code"
@@ -7,7 +7,7 @@ export const useEmailPassLogin = (
options?: UseMutationOptions<void, Error, EmailPassReq> options?: UseMutationOptions<void, Error, EmailPassReq>
) => { ) => {
return useMutation({ return useMutation({
mutationFn: (payload) => sdk.auth.login("admin", "emailpass", payload), mutationFn: (payload) => sdk.auth.login("user", "emailpass", payload),
onSuccess: async (data, variables, context) => { onSuccess: async (data, variables, context) => {
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
@@ -26,7 +26,7 @@ export const useCreateAuthUser = (
options?: UseMutationOptions<{ token: string }, Error, EmailPassReq> options?: UseMutationOptions<{ token: string }, Error, EmailPassReq>
) => { ) => {
return useMutation({ return useMutation({
mutationFn: (payload) => sdk.auth.create("admin", "emailpass", payload), mutationFn: (payload) => sdk.auth.create("user", "emailpass", payload),
onSuccess: async (data, variables, context) => { onSuccess: async (data, variables, context) => {
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
+5 -5
View File
@@ -10,13 +10,13 @@ export class Auth {
this.config = config this.config = config
} }
public login = async ( login = async (
scope: "admin" | "store", actor: "customer" | "user",
method: "emailpass", method: "emailpass",
payload: { email: string; password: string } payload: { email: string; password: string }
) => { ) => {
const { token } = await this.client.fetch<{ token: string }>( const { token } = await this.client.fetch<{ token: string }>(
`/auth/${scope}/${method}`, `/auth/${actor}/${method}`,
{ {
method: "POST", method: "POST",
body: payload, body: payload,
@@ -45,11 +45,11 @@ export class Auth {
} }
create = async ( create = async (
scope: "admin" | "store", actor: "customer" | "user",
method: "emailpass", method: "emailpass",
payload: { email: string; password: string } payload: { email: string; password: string }
): Promise<{ token: string }> => { ): Promise<{ token: string }> => {
return await this.client.fetch(`/auth/${scope}/${method}`, { return await this.client.fetch(`/auth/${actor}/${method}`, {
method: "POST", method: "POST",
body: payload, body: payload,
}) })
@@ -22,12 +22,6 @@ export type AuthIdentityDTO = {
*/ */
entity_id: string 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. * 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. * Holds custom data related to the user in key-value pairs.
*/ */
user_metadata: Record<string, unknown> 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 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. * 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. * Holds custom data related to the user in key-value pairs.
*/ */
user_metadata?: Record<string, unknown> 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. * Holds custom data related to the user in key-value pairs.
*/ */
user_metadata?: Record<string, unknown> 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 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 * @interface
* *
@@ -77,30 +52,25 @@ export type AuthenticationInput = {
/** /**
* URL of the incoming authentication request. * URL of the incoming authentication request.
*/ */
url: string url?: string
/** /**
* Headers of incoming authentication request. * Headers of incoming authentication request.
*/ */
headers: Record<string, string> headers?: Record<string, string>
/** /**
* Query params of the incoming authentication request. * Query params of the incoming authentication request.
*/ */
query: Record<string, string> query?: Record<string, string>
/** /**
* Body of the incoming authentication request. * Body of the incoming authentication request.
*/ */
body: Record<string, string> body?: Record<string, string>
/**
* Scope for the authentication request.
*/
authScope: string
/** /**
* Protocol of the incoming authentication request (For example, `https`). * Protocol of the incoming authentication request (For example, `https`).
*/ */
protocol: string protocol?: string
} }
+2
View File
@@ -1,2 +1,4 @@
export * from "./service" export * from "./service"
export * from "./common" export * from "./common"
export * from "./provider"
export * from "./providers"
+32
View 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>
}
@@ -0,0 +1,7 @@
export interface EmailPassAuthProviderOptions {
hashConfig?: {
logN: number
r: number
p: number
}
}
@@ -0,0 +1 @@
export * from "./emailpass"
-11
View File
@@ -43,7 +43,6 @@ export interface IAuthModuleService extends IModuleService {
* headers: req.headers, * headers: req.headers,
* query: req.query, * query: req.query,
* body: req.body, * body: req.body,
* authScope: "admin",
* protocol: req.protocol, * protocol: req.protocol,
* } as AuthenticationInput) * } as AuthenticationInput)
* ``` * ```
@@ -81,7 +80,6 @@ export interface IAuthModuleService extends IModuleService {
* headers: req.headers, * headers: req.headers,
* query: req.query, * query: req.query,
* body: req.body, * body: req.body,
* authScope: "admin",
* protocol: req.protocol, * protocol: req.protocol,
* } as AuthenticationInput) * } as AuthenticationInput)
* ``` * ```
@@ -200,12 +198,10 @@ export interface IAuthModuleService extends IModuleService {
* { * {
* provider: "emailpass", * provider: "emailpass",
* entity_id: "user@example.com", * entity_id: "user@example.com",
* scope: "admin",
* }, * },
* { * {
* provider: "google", * provider: "google",
* entity_id: "user@gmail.com", * entity_id: "user@gmail.com",
* scope: "email profile",
* }, * },
* ]) * ])
*/ */
@@ -225,7 +221,6 @@ export interface IAuthModuleService extends IModuleService {
* const authIdentity = await authModuleService.create({ * const authIdentity = await authModuleService.create({
* provider: "emailpass", * provider: "emailpass",
* entity_id: "user@example.com", * entity_id: "user@example.com",
* scope: "admin",
* }) * })
*/ */
create( create(
@@ -244,9 +239,6 @@ export interface IAuthModuleService extends IModuleService {
* const authIdentities = await authModuleService.update([ * const authIdentities = await authModuleService.update([
* { * {
* id: "authusr_123", * id: "authusr_123",
* app_metadata: {
* test: true,
* },
* }, * },
* ]) * ])
*/ */
@@ -265,9 +257,6 @@ export interface IAuthModuleService extends IModuleService {
* @example * @example
* const authIdentity = await authModuleService.update({ * const authIdentity = await authModuleService.update({
* id: "authusr_123", * id: "authusr_123",
* app_metadata: {
* test: true,
* },
* }) * })
*/ */
update( update(
@@ -705,6 +705,35 @@ export type ProjectConfigOptions = {
* ``` * ```
*/ */
adminCors: string 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 implements IAuthProvider {
private static PROVIDER: string
export abstract class AbstractAuthModuleProvider { private static DISPLAY_NAME: string
public static PROVIDER: string
public static DISPLAY_NAME: string
protected readonly container_: any protected readonly container_: any
protected scopeConfig_: AuthProviderScope
protected scope_: string
private readonly scopes_: Record<string, AuthProviderScope>
public get provider() { public get provider() {
return (this.constructor as typeof AbstractAuthModuleProvider).PROVIDER return (this.constructor as typeof AbstractAuthModuleProvider).PROVIDER
} }
@@ -20,43 +18,22 @@ export abstract class AbstractAuthModuleProvider {
return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME
} }
protected constructor( protected constructor({}, config: { provider: string; displayName: string }) {
{ scopes },
config: { provider: string; displayName: string }
) {
this.container_ = arguments[0] this.container_ = arguments[0]
this.scopes_ = scopes
;(this.constructor as typeof AbstractAuthModuleProvider).PROVIDER ??= ;(this.constructor as typeof AbstractAuthModuleProvider).PROVIDER ??=
config.provider config.provider
;(this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME ??= ;(this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME ??=
config.displayName 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( abstract authenticate(
data: Record<string, unknown> data: AuthenticationInput,
authIdentityProviderService: AuthIdentityProviderService
): Promise<AuthenticationResponse> ): Promise<AuthenticationResponse>
public validateCallback( validateCallback(
data: Record<string, unknown> data: AuthenticationInput,
authIdentityProviderService: AuthIdentityProviderService
): Promise<AuthenticationResponse> { ): Promise<AuthenticationResponse> {
throw new Error( throw new Error(
`Callback authentication not implemented for provider ${this.provider}` `Callback authentication not implemented for provider ${this.provider}`
@@ -16,7 +16,7 @@ import { createLinkBody } from "../../utils/validators"
export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/api-keys*", matcher: "/admin/api-keys*",
middlewares: [authenticate("admin", ["bearer", "session"])], middlewares: [authenticate("user", ["bearer", "session"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -14,7 +14,7 @@ import {
export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [ export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/campaigns*", matcher: "/admin/campaigns*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -15,7 +15,7 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/collections*", 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"], method: ["ALL"],
matcher: "/admin/currencies*", matcher: "/admin/currencies*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -15,7 +15,7 @@ export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/customer-groups*", matcher: "/admin/customer-groups*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -19,7 +19,7 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/customers*", matcher: "/admin/customers*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -13,7 +13,7 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/draft-orders*", matcher: "/admin/draft-orders*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -8,7 +8,7 @@ export const adminFulfillmentProvidersRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/fulfillment-providers*", matcher: "/admin/fulfillment-providers*",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -14,7 +14,7 @@ export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/fulfillment-sets*", matcher: "/admin/fulfillment-sets*",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["POST"], method: ["POST"],
@@ -14,7 +14,7 @@ export const adminFulfillmentsRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/fulfillments*", matcher: "/admin/fulfillments*",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["POST"], method: ["POST"],
@@ -20,7 +20,7 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/inventory-items*", matcher: "/admin/inventory-items*",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -18,7 +18,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/admin/invites", matcher: "/admin/invites",
middlewares: [ middlewares: [
authenticate("admin", ["session", "bearer", "api-key"]), authenticate("user", ["session", "bearer", "api-key"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetInvitesParams, AdminGetInvitesParams,
QueryConfig.listTransformQueryConfig QueryConfig.listTransformQueryConfig
@@ -29,7 +29,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"], method: ["POST"],
matcher: "/admin/invites", matcher: "/admin/invites",
middlewares: [ middlewares: [
authenticate("admin", ["session", "bearer", "api-key"]), authenticate("user", ["session", "bearer", "api-key"]),
validateAndTransformBody(AdminCreateInvite), validateAndTransformBody(AdminCreateInvite),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetInviteParams, AdminGetInviteParams,
@@ -41,7 +41,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
method: "POST", method: "POST",
matcher: "/admin/invites/accept", matcher: "/admin/invites/accept",
middlewares: [ middlewares: [
authenticate("admin", ["session", "bearer"], { authenticate("user", ["session", "bearer"], {
allowUnregistered: true, allowUnregistered: true,
}), }),
validateAndTransformBody(AdminInviteAccept), validateAndTransformBody(AdminInviteAccept),
@@ -55,7 +55,7 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/admin/invites/:id", matcher: "/admin/invites/:id",
middlewares: [ middlewares: [
authenticate("admin", ["session", "bearer", "api-key"]), authenticate("user", ["session", "bearer", "api-key"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetInviteParams, AdminGetInviteParams,
QueryConfig.retrieveTransformQueryConfig QueryConfig.retrieveTransformQueryConfig
@@ -65,13 +65,13 @@ export const adminInviteRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["DELETE"], method: ["DELETE"],
matcher: "/admin/invites/:id", matcher: "/admin/invites/:id",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: "POST", method: "POST",
matcher: "/admin/invites/:id/resend", matcher: "/admin/invites/:id/resend",
middlewares: [ middlewares: [
authenticate("admin", ["session", "bearer", "api-key"]), authenticate("user", ["session", "bearer", "api-key"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetInviteParams, AdminGetInviteParams,
QueryConfig.retrieveTransformQueryConfig QueryConfig.retrieveTransformQueryConfig
@@ -14,7 +14,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/orders*", matcher: "/admin/orders*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -16,7 +16,7 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/payments", matcher: "/admin/payments",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -18,7 +18,7 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/price-lists*", matcher: "/admin/price-lists*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -13,7 +13,7 @@ import {
export const adminPricingRoutesMiddlewares: MiddlewareRoute[] = [ export const adminPricingRoutesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/pricing*", matcher: "/admin/pricing*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -15,7 +15,7 @@ export const adminProductCategoryRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/product-categories*", matcher: "/admin/product-categories*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -14,7 +14,7 @@ export const adminProductTypeRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/product-types/*", 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"], method: ["ALL"],
matcher: "/admin/products*", matcher: "/admin/products*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -19,7 +19,7 @@ import {
export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/promotions*", matcher: "/admin/promotions*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -14,7 +14,7 @@ export const adminRegionRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/regions*", matcher: "/admin/regions*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -15,7 +15,7 @@ export const adminReservationRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/reservations*", matcher: "/admin/reservations*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -13,7 +13,7 @@ export const adminReturnReasonRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/return-reasons*", matcher: "/admin/return-reasons*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -13,7 +13,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/returns*", matcher: "/admin/returns*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -16,7 +16,7 @@ export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/sales-channels*", matcher: "/admin/sales-channels*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -21,7 +21,7 @@ import { createBatchBody } from "../../utils/validators"
export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/shipping-options*", matcher: "/admin/shipping-options*",
middlewares: [authenticate("admin", ["bearer", "session"])], middlewares: [authenticate("user", ["bearer", "session"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -15,7 +15,7 @@ import {
export const adminShippingProfilesMiddlewares: MiddlewareRoute[] = [ export const adminShippingProfilesMiddlewares: MiddlewareRoute[] = [
{ {
matcher: "/admin/shipping-profiles*", matcher: "/admin/shipping-profiles*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["POST"], method: ["POST"],
@@ -17,7 +17,7 @@ export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/stock-locations*", matcher: "/admin/stock-locations*",
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
}, },
{ {
method: ["POST"], method: ["POST"],
@@ -13,7 +13,7 @@ export const adminStoreRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/stores*", matcher: "/admin/stores*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -17,7 +17,7 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/admin/tax-rates*", matcher: "/admin/tax-rates*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: "POST", method: "POST",
@@ -15,7 +15,7 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/tax-regions*", matcher: "/admin/tax-regions*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: "POST", method: "POST",
@@ -14,7 +14,7 @@ export const adminUploadRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/uploads*", 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 // 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"], method: ["GET"],
matcher: "/admin/users", matcher: "/admin/users",
middlewares: [ middlewares: [
authenticate("admin", ["bearer", "session"]), authenticate("user", ["bearer", "session"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetUsersParams, AdminGetUsersParams,
QueryConfig.listTransformQueryConfig QueryConfig.listTransformQueryConfig
@@ -28,7 +28,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"], method: ["POST"],
matcher: "/admin/users", matcher: "/admin/users",
middlewares: [ middlewares: [
authenticate("admin", ["bearer", "session"], { allowUnregistered: true }), authenticate("user", ["bearer", "session"], { allowUnregistered: true }),
validateAndTransformBody(AdminCreateUser), validateAndTransformBody(AdminCreateUser),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetUserParams, AdminGetUserParams,
@@ -40,7 +40,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/admin/users/:id", matcher: "/admin/users/:id",
middlewares: [ middlewares: [
authenticate("admin", ["bearer", "session"]), authenticate("user", ["bearer", "session"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetUserParams, AdminGetUserParams,
QueryConfig.retrieveTransformQueryConfig QueryConfig.retrieveTransformQueryConfig
@@ -51,7 +51,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/admin/users/me", matcher: "/admin/users/me",
middlewares: [ middlewares: [
authenticate("admin", ["bearer", "session"]), authenticate("user", ["bearer", "session"]),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetUserParams, AdminGetUserParams,
QueryConfig.retrieveTransformQueryConfig QueryConfig.retrieveTransformQueryConfig
@@ -62,7 +62,7 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"], method: ["POST"],
matcher: "/admin/users/:id", matcher: "/admin/users/:id",
middlewares: [ middlewares: [
authenticate("admin", ["bearer", "session"]), authenticate("user", ["bearer", "session"]),
validateAndTransformBody(AdminUpdateUser), validateAndTransformBody(AdminUpdateUser),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetUserParams, AdminGetUserParams,
@@ -73,6 +73,6 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: ["DELETE"], method: ["DELETE"],
matcher: "/admin/users/:id", 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_id: result.id,
actor_type: "user", actor_type: "user",
auth_identity_id: req.auth_context.auth_identity_id, auth_identity_id: req.auth_context.auth_identity_id,
app_metadata: {},
scope: "admin",
}, },
{ {
secret: jwtSecret, secret: jwtSecret,
@@ -16,7 +16,7 @@ export const adminWorkflowsExecutionsMiddlewares: MiddlewareRoute[] = [
{ {
method: ["ALL"], method: ["ALL"],
matcher: "/admin/workflows-executions*", matcher: "/admin/workflows-executions*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], middlewares: [authenticate("user", ["bearer", "session", "api-key"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -1,5 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" import {
AuthenticationInput,
IAuthModuleService,
ConfigModule,
} from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
MedusaError, MedusaError,
@@ -9,10 +13,23 @@ import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { generateJwtToken } from "../../../../utils/auth/token" import { generateJwtToken } from "../../../../utils/auth/token"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => { export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, auth_provider } = req.params const { actor_type, auth_provider } = req.params
const actorType = scope === "admin" ? "user" : "customer"
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) 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( const service: IAuthModuleService = req.scope.resolve(
ModuleRegistrationName.AUTH ModuleRegistrationName.AUTH
) )
@@ -22,7 +39,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
headers: req.headers, headers: req.headers,
query: req.query, query: req.query,
body: req.body, body: req.body,
authScope: scope,
protocol: req.protocol, protocol: req.protocol,
} as AuthenticationInput } as AuthenticationInput
@@ -31,11 +47,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const queryObject = remoteQueryObjectFromString({ const queryObject = remoteQueryObjectFromString({
entryPoint: "auth_identity", entryPoint: "auth_identity",
fields: [`${actorType}.id`], fields: [`${actor_type}.id`],
variables: { id: authIdentity.id }, variables: { id: authIdentity.id },
}) })
const [actorData] = await remoteQuery(queryObject) const [actorData] = await remoteQuery(queryObject)
const entityId = actorData?.[actorType]?.id const entityId = actorData?.[actor_type]?.id
if (success) { if (success) {
const { http } = req.scope.resolve( const { http } = req.scope.resolve(
@@ -43,14 +59,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
).projectConfig ).projectConfig
const { jwtSecret, jwtExpiresIn } = http const { jwtSecret, jwtExpiresIn } = http
// TODO: Clean up mapping between scope and actor type
const token = generateJwtToken( const token = generateJwtToken(
{ {
actor_id: entityId, actor_id: entityId,
actor_type: actorType, actor_type,
auth_identity_id: authIdentity.id, auth_identity_id: authIdentity.id,
app_metadata: {},
scope,
}, },
{ {
secret: jwtSecret, secret: jwtSecret,
@@ -1,5 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" import {
AuthenticationInput,
IAuthModuleService,
ConfigModule,
} from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
MedusaError, MedusaError,
@@ -9,9 +13,22 @@ import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { generateJwtToken } from "../../../utils/auth/token" import { generateJwtToken } from "../../../utils/auth/token"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => { export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, auth_provider } = req.params const { actor_type, auth_provider } = req.params
const actorType = scope === "admin" ? "user" : "customer"
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) 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( const service: IAuthModuleService = req.scope.resolve(
ModuleRegistrationName.AUTH ModuleRegistrationName.AUTH
@@ -22,7 +39,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
headers: req.headers, headers: req.headers,
query: req.query, query: req.query,
body: req.body, body: req.body,
authScope: scope,
protocol: req.protocol, protocol: req.protocol,
} as AuthenticationInput } as AuthenticationInput
@@ -43,21 +59,18 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const queryObject = remoteQueryObjectFromString({ const queryObject = remoteQueryObjectFromString({
entryPoint: "auth_identity", entryPoint: "auth_identity",
fields: [`${actorType}.id`], fields: [`${actor_type}.id`],
variables: { id: authIdentity.id }, variables: { id: authIdentity.id },
}) })
const [actorData] = await remoteQuery(queryObject) const [actorData] = await remoteQuery(queryObject)
const entityId = actorData?.[actorType]?.id const entityId = actorData?.[actor_type]?.id
const { jwtSecret, jwtExpiresIn } = http const { jwtSecret, jwtExpiresIn } = http
// TODO: Clean up mapping between scope and actor type
const token = generateJwtToken( const token = generateJwtToken(
{ {
actor_id: entityId, actor_id: entityId,
actor_type: actorType, actor_type,
auth_identity_id: authIdentity.id, auth_identity_id: authIdentity.id,
app_metadata: {},
scope,
}, },
{ {
secret: jwtSecret, secret: jwtSecret,
+2 -2
View File
@@ -14,12 +14,12 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
}, },
{ {
method: ["POST"], method: ["POST"],
matcher: "/auth/:scope/:auth_provider/callback", matcher: "/auth/:actor_type/:auth_provider/callback",
middlewares: [], middlewares: [],
}, },
{ {
method: ["POST"], method: ["POST"],
matcher: "/auth/:scope/:auth_provider", matcher: "/auth/:actor_type/:auth_provider",
middlewares: [], middlewares: [],
}, },
] ]
@@ -25,7 +25,7 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
method: "ALL", method: "ALL",
matcher: "/store/carts*", matcher: "/store/carts*",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"], { authenticate("customer", ["session", "bearer"], {
allowUnauthenticated: true, allowUnauthenticated: true,
}), }),
], ],
@@ -20,7 +20,9 @@ export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"], method: ["POST"],
matcher: "/store/customers", matcher: "/store/customers",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"], { allowUnregistered: true }), authenticate("customer", ["session", "bearer"], {
allowUnregistered: true,
}),
validateAndTransformBody(StoreCreateCustomer), validateAndTransformBody(StoreCreateCustomer),
validateAndTransformQuery( validateAndTransformQuery(
StoreGetCustomerParams, StoreGetCustomerParams,
@@ -31,7 +33,7 @@ export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
matcher: "/store/customers/me*", matcher: "/store/customers/me*",
middlewares: [authenticate("store", ["session", "bearer"])], middlewares: [authenticate("customer", ["session", "bearer"])],
}, },
{ {
method: ["GET"], method: ["GET"],
@@ -41,8 +41,6 @@ export const POST = async (
actor_id: result.id, actor_id: result.id,
actor_type: "customer", actor_type: "customer",
auth_identity_id: req.auth_context.auth_identity_id, auth_identity_id: req.auth_context.auth_identity_id,
app_metadata: {},
scope: "store",
}, },
{ {
secret: jwtSecret, secret: jwtSecret,
@@ -9,7 +9,7 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/store/orders", matcher: "/store/orders",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"]), authenticate("customer", ["session", "bearer"]),
validateAndTransformQuery( validateAndTransformQuery(
StoreGetOrdersParams, StoreGetOrdersParams,
QueryConfig.listTransformQueryConfig QueryConfig.listTransformQueryConfig
@@ -20,7 +20,7 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"], method: ["GET"],
matcher: "/store/orders/:id", matcher: "/store/orders/:id",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"], { authenticate("customer", ["session", "bearer"], {
allowUnauthenticated: true, allowUnauthenticated: true,
}), }),
validateAndTransformQuery( validateAndTransformQuery(
@@ -14,7 +14,7 @@ export const storePaymentCollectionsMiddlewares: MiddlewareRoute[] = [
method: "ALL", method: "ALL",
matcher: "/store/payment-collections*", matcher: "/store/payment-collections*",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"], { authenticate("customer", ["session", "bearer"], {
allowUnauthenticated: true, allowUnauthenticated: true,
}), }),
], ],
@@ -19,7 +19,7 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
method: "ALL", method: "ALL",
matcher: "/store/products*", matcher: "/store/products*",
middlewares: [ middlewares: [
authenticate("store", ["session", "bearer"], { authenticate("customer", ["session", "bearer"], {
allowUnauthenticated: true, allowUnauthenticated: true,
}), }),
], ],
+6 -2
View File
@@ -46,14 +46,18 @@ export default async function ({
} else { } else {
const user = await userService.create({ email }) const user = await userService.create({ email })
const { authIdentity } = await authService.authenticate(provider, { const { authIdentity, error } = await authService.authenticate(provider, {
body: { body: {
email, email,
password, password,
}, },
authScope: "admin",
}) })
if (error) {
Logger.error(error)
throw new Error(error)
}
await remoteLink.create([ await remoteLink.create([
{ {
[Modules.USER]: { [Modules.USER]: {
+1 -4
View File
@@ -61,11 +61,8 @@ export interface MedusaRequest<Body = unknown>
export interface AuthContext { export interface AuthContext {
actor_id: string actor_id: string
// TODO: We possibly want to make this more open-ended so it's easy to extend. actor_type: string
actor_type: "api-key" | "user" | "customer" | "unknown"
auth_identity_id: string auth_identity_id: string
scope: string
app_metadata: Record<string, any>
} }
export interface AuthenticatedMedusaRequest<Body = never> export interface AuthenticatedMedusaRequest<Body = never>
@@ -8,25 +8,23 @@ import {
MedusaRequest, MedusaRequest,
MedusaResponse, MedusaResponse,
} from "../../types/routing" } from "../../types/routing"
import { ContainerRegistrationKeys } from "@medusajs/utils"
const SESSION_AUTH = "session" const SESSION_AUTH = "session"
const BEARER_AUTH = "bearer" const BEARER_AUTH = "bearer"
const API_KEY_AUTH = "api-key" 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 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 = { type MedusaSession = {
auth_context: AuthContext auth_context: AuthContext
} }
export const authenticate = ( export const authenticate = (
authScope: Scope | Scope[], actorType: string,
authType: AuthType | AuthType[], authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {} options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
): RequestHandler => { ): RequestHandler => {
@@ -36,21 +34,20 @@ export const authenticate = (
next: NextFunction next: NextFunction
): Promise<void> => { ): Promise<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType] 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 const req_ = req as AuthenticatedMedusaRequest
// We only allow authenticating using a secret API key on the admin // We only allow authenticating using a secret API key on the admin
const isExclusivelyAdmin = const isExclusivelyUser =
scopes.length === 1 && scopes.includes(ADMIN_SCOPE) actorTypes.length === 1 && actorTypes[0] === ADMIN_ACTOR_TYPE
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyAdmin) {
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyUser) {
const apiKey = await getApiKeyInfo(req) const apiKey = await getApiKeyInfo(req)
if (apiKey) { if (apiKey) {
req_.auth_context = { req_.auth_context = {
actor_id: apiKey.id, actor_id: apiKey.id,
actor_type: "api-key", actor_type: "api-key",
auth_identity_id: "", auth_identity_id: "",
app_metadata: {},
scope: ADMIN_SCOPE,
} }
return next() return next()
@@ -61,27 +58,29 @@ export const authenticate = (
let authContext: AuthContext | null = getAuthContextFromSession( let authContext: AuthContext | null = getAuthContextFromSession(
req.session, req.session,
authTypes, authTypes,
scopes actorTypes
) )
if (!authContext) { if (!authContext) {
const { http } = const { http } = req.scope.resolve<ConfigModule>(
req.scope.resolve<ConfigModule>("configModule").projectConfig ContainerRegistrationKeys.CONFIG_MODULE
).projectConfig
authContext = getAuthContextFromJwtToken( authContext = getAuthContextFromJwtToken(
req.headers.authorization, req.headers.authorization,
http.jwtSecret!, http.jwtSecret!,
authTypes, authTypes,
scopes actorTypes
) )
} }
// If the entity is authenticated, and it is a registered user/customer we can continue // If the entity is authenticated, and it is a registered actor we can continue
if (!!authContext?.actor_id && authContext.actor_type !== "unknown") { if (authContext?.actor_id) {
req_.auth_context = authContext req_.auth_context = authContext
return next() 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) { if (authContext?.auth_identity_id && options.allowUnregistered) {
req_.auth_context = authContext req_.auth_context = authContext
return next() return next()
@@ -142,7 +141,7 @@ const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
const getAuthContextFromSession = ( const getAuthContextFromSession = (
session: Partial<MedusaSession> = {}, session: Partial<MedusaSession> = {},
authTypes: AuthType[], authTypes: AuthType[],
scopes: Scope[] actorTypes: string[]
): AuthContext | null => { ): AuthContext | null => {
if (!authTypes.includes(SESSION_AUTH)) { if (!authTypes.includes(SESSION_AUTH)) {
return null return null
@@ -150,8 +149,8 @@ const getAuthContextFromSession = (
if ( if (
session.auth_context && session.auth_context &&
(scopes.includes("*") || (actorTypes.includes("*") ||
scopes.includes(session.auth_context.scope as Scope)) actorTypes.includes(session.auth_context.actor_type))
) { ) {
return session.auth_context return session.auth_context
} }
@@ -163,7 +162,7 @@ const getAuthContextFromJwtToken = (
authHeader: string | undefined, authHeader: string | undefined,
jwtSecret: string, jwtSecret: string,
authTypes: AuthType[], authTypes: AuthType[],
scopes: Scope[] actorTypes: string[]
): AuthContext | null => { ): AuthContext | null => {
if (!authTypes.includes(BEARER_AUTH)) { if (!authTypes.includes(BEARER_AUTH)) {
return null return null
@@ -185,7 +184,10 @@ const getAuthContextFromJwtToken = (
// verify token and set authUser // verify token and set authUser
try { try {
const verified = jwt.verify(token, jwtSecret) as JwtPayload 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 return verified as AuthContext
} }
} catch (err) { } catch (err) {
@@ -8,18 +8,15 @@ export async function createAuthIdentities(
id: "test-id", id: "test-id",
entity_id: "test-id", entity_id: "test-id",
provider: "manual", provider: "manual",
scope: "store",
}, },
{ {
id: "test-id-1", id: "test-id-1",
entity_id: "test-id-1", entity_id: "test-id-1",
provider: "manual", provider: "manual",
scope: "store",
}, },
{ {
entity_id: "test-id-2", entity_id: "test-id-2",
provider: "store", provider: "store",
scope: "store",
}, },
] ]
): Promise<AuthIdentity[]> { ): 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 { IAuthModuleService } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk" 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" import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -216,7 +216,6 @@ moduleIntegrationTestRunner({
id: "test", id: "test",
provider: "manual", provider: "manual",
entity_id: "test", 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",
})
})
})
})
},
})
+1
View File
@@ -48,6 +48,7 @@
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
"dependencies": { "dependencies": {
"@medusajs/auth-emailpass": "0.0.1",
"@medusajs/modules-sdk": "^1.12.9", "@medusajs/modules-sdk": "^1.12.9",
"@medusajs/types": "^1.11.14", "@medusajs/types": "^1.11.14",
"@medusajs/utils": "^1.11.7", "@medusajs/utils": "^1.11.7",
+40 -55
View File
@@ -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 { import {
AuthModuleProviderConfig, AuthIdentifiersRegistrationName,
AuthProviderScope, AuthProviderRegistrationPrefix,
LoaderOptions, } from "@types"
ModulesSdkTypes,
} from "@medusajs/types"
import {
AwilixContainer,
ClassOrFunctionReturning,
Constructor,
Resolver,
asClass,
} from "awilix"
type AuthModuleProviders = { const registrationFn = async (klass, container, pluginOptions) => {
providers: AuthModuleProviderConfig[] 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 ({ export default async ({
@@ -25,48 +30,28 @@ export default async ({
( (
| ModulesSdkTypes.ModuleServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & ) & { providers: ModuleProvider[] }
AuthModuleProviders
>): Promise<void> => { >): Promise<void> => {
const providerMap = new Map( // TODO: Temporary settings used by the starter, remove once the auth module is updated
options?.providers?.map((provider) => [provider.name, provider.scopes]) ?? const isLegacyOptions =
[] options?.providers?.length && !!(options?.providers[0] as any)?.name
)
// if(options?.providers?.length) { // Note: For now we want to inject some providers out of the box
// TODO: implement plugin provider registration const providerConfig = [
// } {
resolve: EmailPassProvider,
options: {
config: {
emailpass: {},
},
},
},
...(isLegacyOptions ? [] : options?.providers ?? []),
]
const providersToLoad = Object.values(defaultProviders) await moduleProviderLoader({
container,
for (const provider of providersToLoad) { providers: providerConfig,
container.register({ registerServiceFn: registrationFn,
[`auth_provider_${provider.PROVIDER}`]: asClass(
provider as Constructor<any>
)
.singleton()
.inject(() => ({ scopes: providerMap.get(provider.PROVIDER) ?? {} })),
})
}
container.register({
[`auth_providers`]: asArray(providersToLoad, providerMap),
}) })
} }
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, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"scope": {
"name": "scope",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"user_metadata": { "user_metadata": {
"name": "user_metadata", "name": "user_metadata",
"type": "jsonb", "type": "jsonb",
@@ -72,8 +63,8 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"keyName": "IDX_auth_identity_provider_scope_entity_id", "keyName": "IDX_auth_identity_provider_entity_id",
"columnNames": ["provider", "scope", "entity_id"], "columnNames": ["provider", "entity_id"],
"composite": true, "composite": true,
"primary": false, "primary": false,
"unique": true "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() @Entity()
@Unique({ @Unique({
properties: ["provider", "scope", "entity_id"], properties: ["provider", "entity_id"],
name: "IDX_auth_identity_provider_scope_entity_id", name: "IDX_auth_identity_provider_entity_id",
}) })
export default class AuthIdentity { export default class AuthIdentity {
[OptionalProps]: OptionalFields [OptionalProps]: OptionalFields
@@ -29,14 +29,11 @@ export default class AuthIdentity {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
provider: string provider: string
@Property({ columnType: "text" })
scope: string
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
user_metadata: Record<string, unknown> | null user_metadata: Record<string, unknown> | null
@Property({ columnType: "jsonb" }) @Property({ columnType: "jsonb", nullable: true })
app_metadata: Record<string, unknown> = {} app_metadata: Record<string, unknown> | null
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
provider_metadata: Record<string, unknown> | null = null provider_metadata: Record<string, unknown> | null = null
@@ -1,5 +1,5 @@
import { AuthModuleService } from "@services"
import { ModuleExports } from "@medusajs/types" import { ModuleExports } from "@medusajs/types"
import { AuthModuleService } from "@services"
import loadProviders from "./loaders/providers" import loadProviders from "./loaders/providers"
const service = AuthModuleService 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
+17 -17
View File
@@ -1,13 +1,12 @@
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils" import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
import { AuthIdentityService } from "@services"
import jwt, { JwtPayload } from "jsonwebtoken" import jwt, { JwtPayload } from "jsonwebtoken"
import { AuthorizationCode } from "simple-oauth2" import { AuthorizationCode } from "simple-oauth2"
import url from "url" import url from "url"
type InjectedDependencies = { type InjectedDependencies = {
authIdentityService: AuthIdentityService authIdentityService: any
} }
type ProviderConfig = { type ProviderConfig = {
@@ -18,15 +17,12 @@ type ProviderConfig = {
} }
class GoogleProvider extends AbstractAuthModuleProvider { class GoogleProvider extends AbstractAuthModuleProvider {
public static PROVIDER = "google" protected readonly authIdentityService_: any
public static DISPLAY_NAME = "Google Authentication"
protected readonly authIdentityService_: AuthIdentityService constructor({ authIdentityService }: InjectedDependencies, options: any) {
constructor({ authIdentityService }: InjectedDependencies) {
super(arguments[0], { super(arguments[0], {
provider: GoogleProvider.PROVIDER, provider: "google",
displayName: GoogleProvider.DISPLAY_NAME, displayName: "Google Authentication",
}) })
this.authIdentityService_ = authIdentityService this.authIdentityService_ = authIdentityService
@@ -72,6 +68,9 @@ class GoogleProvider extends AbstractAuthModuleProvider {
} }
const code = req.query?.code ?? req.body?.code const code = req.query?.code ?? req.body?.code
if (!code) {
return { success: false, error: "No code provided" }
}
return await this.validateCallbackToken(code, config) return await this.validateCallbackToken(code, config)
} }
@@ -89,16 +88,15 @@ class GoogleProvider extends AbstractAuthModuleProvider {
authIdentity = authIdentity =
await this.authIdentityService_.retrieveByProviderAndEntityId( await this.authIdentityService_.retrieveByProviderAndEntityId(
entity_id, entity_id,
GoogleProvider.PROVIDER this.provider
) )
} catch (error) { } catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) { if (error.type === MedusaError.Types.NOT_FOUND) {
const [createdAuthIdentity] = await this.authIdentityService_.create([ const [createdAuthIdentity] = await this.authIdentityService_.create([
{ {
entity_id, entity_id,
provider: GoogleProvider.PROVIDER, provider: this.provider,
user_metadata: jwtData!.payload, user_metadata: jwtData!.payload,
scope: this.scope_,
}, },
]) ])
authIdentity = createdAuthIdentity authIdentity = createdAuthIdentity
@@ -132,7 +130,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
accessToken.token.id_token accessToken.token.id_token
) )
const { successRedirectUrl } = this.getConfigFromScope() const { successRedirectUrl } = this.getConfig()
return { return {
success, success,
@@ -144,8 +142,10 @@ class GoogleProvider extends AbstractAuthModuleProvider {
} }
} }
private getConfigFromScope(): ProviderConfig { private getConfig(): ProviderConfig {
const config: Partial<ProviderConfig> = { ...this.scopeConfig_ } // TODO: Fetch this from provider config
// const config: Partial<ProviderConfig> = { ...this.scopeConfig_ }
const config = {} as any
if (!config.clientID) { if (!config.clientID) {
throw new Error("Google clientID is required") throw new Error("Google clientID is required")
@@ -163,7 +163,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
} }
private originalURL(req: AuthenticationInput) { private originalURL(req: AuthenticationInput) {
const host = req.headers.host const host = req.headers?.host
const protocol = req.protocol const protocol = req.protocol
const path = req.url || "" const path = req.url || ""
@@ -173,7 +173,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
private async getProviderConfig( private async getProviderConfig(
req: AuthenticationInput req: AuthenticationInput
): Promise<ProviderConfig> { ): Promise<ProviderConfig> {
const config = this.getConfigFromScope() const config = this.getConfig()
const callbackURL = config.callbackURL const callbackURL = config.callbackURL
+1 -2
View File
@@ -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 { import {
AuthenticationInput, AuthenticationInput,
AuthenticationResponse, AuthenticationResponse,
AuthIdentityProviderService,
AuthTypes, AuthTypes,
Context, Context,
DAL, DAL,
@@ -14,16 +15,17 @@ import { AuthIdentity } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { import {
AbstractAuthModuleProvider,
InjectManager, InjectManager,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
} from "@medusajs/utils" } from "@medusajs/utils"
import AuthProviderService from "./auth-provider"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
authIdentityService: ModulesSdkTypes.InternalModuleService<any> authIdentityService: ModulesSdkTypes.InternalModuleService<any>
authProviderService: AuthProviderService
} }
const generateMethodForModels = [AuthIdentity] const generateMethodForModels = [AuthIdentity]
@@ -42,9 +44,14 @@ export default class AuthModuleService<
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected authIdentityService_: ModulesSdkTypes.InternalModuleService<TAuthIdentity> protected authIdentityService_: ModulesSdkTypes.InternalModuleService<TAuthIdentity>
protected readonly authProviderService_: AuthProviderService
constructor( constructor(
{ authIdentityService, baseRepository }: InjectedDependencies, {
authIdentityService,
authProviderService,
baseRepository,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
// @ts-ignore // @ts-ignore
@@ -52,6 +59,7 @@ export default class AuthModuleService<
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.authIdentityService_ = authIdentityService this.authIdentityService_ = authIdentityService
this.authProviderService_ = authProviderService
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
@@ -116,34 +124,16 @@ export default class AuthModuleService<
return Array.isArray(data) ? serializedUsers : serializedUsers[0] 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( async authenticate(
provider: string, provider: string,
authenticationData: AuthenticationInput authenticationData: AuthenticationInput
): Promise<AuthenticationResponse> { ): Promise<AuthenticationResponse> {
try { try {
const registeredProvider = this.getRegisteredAuthenticationProvider( return await this.authProviderService_.authenticate(
provider, provider,
authenticationData authenticationData,
this.getAuthIdentityProviderService()
) )
return await registeredProvider.authenticate(authenticationData)
} catch (error) { } catch (error) {
return { success: false, error: error.message } return { success: false, error: error.message }
} }
@@ -154,14 +144,49 @@ export default class AuthModuleService<
authenticationData: AuthenticationInput authenticationData: AuthenticationInput
): Promise<AuthenticationResponse> { ): Promise<AuthenticationResponse> {
try { try {
const registeredProvider = this.getRegisteredAuthenticationProvider( return await this.authProviderService_.validateCallback(
provider, provider,
authenticationData authenticationData,
this.getAuthIdentityProviderService()
) )
return await registeredProvider.validateCallback(authenticationData)
} catch (error) { } catch (error) {
return { success: false, error: error.message } 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
)
},
}
}
} }
@@ -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 -1
View File
@@ -1,2 +1,2 @@
export { default as AuthModuleService } from "./auth-module" export { default as AuthModuleService } from "./auth-module"
export { default as AuthIdentityService } from "./auth-identity" export { default as AuthProviderService } from "./auth-provider"
+24
View File
@@ -1,5 +1,29 @@
import { ModuleProviderExports } from "@medusajs/types"
import { ModuleServiceInitializeOptions } from "@medusajs/types"
import { Logger } from "@medusajs/types" import { Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = { export type InitializeModuleInjectableDependencies = {
logger?: Logger 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,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: {},
})
)
})
})
@@ -0,0 +1,7 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}
@@ -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"
]
}
@@ -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",
}
}
}
@@ -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
View File
@@ -5092,10 +5092,24 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@medusajs/auth@workspace:*, @medusajs/auth@workspace:packages/modules/auth":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/auth@workspace:packages/modules/auth" resolution: "@medusajs/auth@workspace:packages/modules/auth"
dependencies: dependencies:
"@medusajs/auth-emailpass": 0.0.1
"@medusajs/modules-sdk": ^1.12.9 "@medusajs/modules-sdk": ^1.12.9
"@medusajs/types": ^1.11.14 "@medusajs/types": ^1.11.14
"@medusajs/utils": ^1.11.7 "@medusajs/utils": ^1.11.7