feat(auth): add authentication endpoints (#6265)

**What**
- Add authentication endpoints: 
  - `/auth/[scope]/[provider]` 
  - `/auth/[scope]/[provider]/callback`
- update authenticate-middleware handler
- Add scope field to user
- Add unique constraint on scope and entity_id

note: there's still some remaining work related to jwt auth to be handled, this is mainly focussed on session auth with endpoints



Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com>
This commit is contained in:
Philip KorsholmandSebastian Rindom authored and GitHub committed 2024-02-02 10:45:32 +00:00
1 parent 061c449179
commit 9fda6a6824
31 files changed
+302 -146

No files matched your search

@@ -1,11 +1,12 @@
import { IAuthModuleService, ICustomerModuleService } from "@medusajs/types"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types" import adminSeeder from "../../../../helpers/admin-seeder"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path" import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api" import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -49,6 +50,7 @@ describe("POST /store/customers", () => {
const authUser = await authService.createAuthUser({ const authUser = await authService.createAuthUser({
entity_id: "store_user", entity_id: "store_user",
provider_id: "test", provider_id: "test",
scope: "store",
}) })
const jwt = await authService.generateJwtToken(authUser.id, "store") const jwt = await authService.generateJwtToken(authUser.id, "store")
@@ -13,6 +13,7 @@ export const createAuthenticatedCustomer = async (
const authUser = await authService.createAuthUser({ const authUser = await authService.createAuthUser({
entity_id: "store_user", entity_id: "store_user",
provider_id: "test", provider_id: "test",
scope: "store",
app_metadata: { customer_id: customer.id }, app_metadata: { customer_id: customer.id },
}) })
@@ -1,5 +1,5 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { AuthUser } from "@models" import { AuthUser } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
export async function createAuthUsers( export async function createAuthUsers(
manager: SqlEntityManager, manager: SqlEntityManager,
@@ -8,15 +8,18 @@ export async function createAuthUsers(
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<AuthUser[]> { ): Promise<AuthUser[]> {
@@ -1,12 +1,11 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { AuthUserService } from "@services" import { AuthUserService } from "@services"
import ContainerLoader from "../../../../src/loaders/container"
import { MikroOrmWrapper } from "../../../utils" import { MikroOrmWrapper } from "../../../utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { asValue } from "awilix"
import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthProviders } from "../../../__fixtures__/auth-provider"
import { createAuthUsers } from "../../../__fixtures__/auth-user" import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { createMedusaContainer } from "@medusajs/utils" import { createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import ContainerLoader from "../../../../src/loaders/container"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -229,7 +228,8 @@ describe("AuthUser Service", () => {
{ {
id: "test", id: "test",
provider_id: "manual", provider_id: "manual",
entity_id: "test" entity_id: "test",
scope: "store"
}, },
]) ])
@@ -246,6 +246,7 @@ describe("AuthModuleService - AuthUser", () => {
id: "test", id: "test",
provider_id: "manual", provider_id: "manual",
entity_id: "test", entity_id: "test",
scope: "store",
}, },
]) ])
@@ -83,7 +83,7 @@ describe("AuthModuleService - AuthProvider", () => {
const { success, error } = await service.authenticate( const { success, error } = await service.authenticate(
"emailpass", "emailpass",
{ {
scope: "non-existing", authScope: "non-existing",
} as any } as any
) )
@@ -1,6 +1,6 @@
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaModule, Modules } from "@medusajs/modules-sdk" import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
import { MikroOrmWrapper } from "../../../utils" import { MikroOrmWrapper } from "../../../utils"
import Scrypt from "scrypt-kdf" import Scrypt from "scrypt-kdf"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
@@ -62,6 +62,7 @@ describe("AuthModuleService - AuthProvider", () => {
{ {
provider: "emailpass", provider: "emailpass",
entity_id: email, entity_id: email,
scope: "store",
provider_metadata: { provider_metadata: {
password: passwordHash, password: passwordHash,
}, },
@@ -73,8 +74,8 @@ describe("AuthModuleService - AuthProvider", () => {
email: "test@test.com", email: "test@test.com",
password: password, password: password,
}, },
scope: "store", authScope: "store",
}) } as any)
expect(res).toEqual({ expect(res).toEqual({
success: true, success: true,
@@ -92,8 +93,8 @@ describe("AuthModuleService - AuthProvider", () => {
const res = await service.authenticate("emailpass", { const res = await service.authenticate("emailpass", {
body: { email: "test@test.com" }, body: { email: "test@test.com" },
scope: "store", authScope: "store",
}) } as any)
expect(res).toEqual({ expect(res).toEqual({
success: false, success: false,
@@ -106,8 +107,8 @@ describe("AuthModuleService - AuthProvider", () => {
const res = await service.authenticate("emailpass", { const res = await service.authenticate("emailpass", {
body: { password: "supersecret" }, body: { password: "supersecret" },
scope: "store", authScope: "store",
}) } as any)
expect(res).toEqual({ expect(res).toEqual({
success: false, success: false,
@@ -127,6 +128,7 @@ describe("AuthModuleService - AuthProvider", () => {
// Add authenticated user // Add authenticated user
{ {
provider: "emailpass", provider: "emailpass",
scope: "store",
entity_id: email, entity_id: email,
provider_metadata: { provider_metadata: {
password_hash: passwordHash, password_hash: passwordHash,
@@ -139,8 +141,8 @@ describe("AuthModuleService - AuthProvider", () => {
email: "test@test.com", email: "test@test.com",
password: "password", password: "password",
}, },
scope: "store", authScope: "store",
}) } as any)
expect(res).toEqual({ expect(res).toEqual({
success: false, success: false,
+1 -1
View File
@@ -55,7 +55,7 @@
"@mikro-orm/migrations": "5.9.7", "@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7", "@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0", "awilix": "^8.0.0",
"dotenv": "^16.1.4", "dotenv": "16.3.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"knex": "2.4.2", "knex": "2.4.2",
"scrypt-kdf": "^2.0.1", "scrypt-kdf": "^2.0.1",
@@ -24,20 +24,14 @@
"nullable": false, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"domain": { "scope": {
"name": "domain", "name": "scope",
"type": "text", "type": "text",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": true,
"default": "'all'", "mappedType": "text"
"enumItems": [
"all",
"store",
"admin"
],
"mappedType": "enum"
}, },
"config": { "config": {
"name": "config", "name": "config",
@@ -104,6 +98,15 @@
"nullable": true, "nullable": true,
"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",
@@ -119,7 +122,7 @@
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": false,
"mappedType": "json" "mappedType": "json"
}, },
"provider_metadata": { "provider_metadata": {
@@ -136,9 +139,10 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"keyName": "IDX_auth_user_provider_entity_id", "keyName": "IDX_auth_user_provider_scope_entity_id",
"columnNames": [ "columnNames": [
"provider_id", "provider_id",
"scope",
"entity_id" "entity_id"
], ],
"composite": true, "composite": true,
@@ -1,30 +0,0 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20240122041959 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'
)
this.addSql(
'create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'
)
this.addSql(
'alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'
)
this.addSql(
'alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'
)
}
async down(): Promise<void> {
this.addSql(
'alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";'
)
this.addSql('drop table if exists "auth_provider" cascade;')
this.addSql('drop table if exists "auth_user" cascade;')
}
}
@@ -0,0 +1,22 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240201100135 extends Migration {
async up(): Promise<void> {
this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "scope" text null, "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));');
this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "scope" text not null, "user_metadata" jsonb null, "app_metadata" jsonb not null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));');
this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_scope_entity_id" unique ("provider_id", "scope", "entity_id");');
this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;');
}
async down(): Promise<void> {
this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";');
this.addSql('drop table if exists "auth_provider" cascade;');
this.addSql('drop table if exists "auth_user" cascade;');
}
}
+2 -2
View File
@@ -20,8 +20,8 @@ export default class AuthProvider {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
name: string name: string
@Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL }) @Property({ columnType: "text", nullable: true })
domain: ProviderDomain = ProviderDomain.ALL scope: string
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
config: Record<string, unknown> | null = null config: Record<string, unknown> | null = null
+10 -4
View File
@@ -17,7 +17,10 @@ import { generateEntityId } from "@medusajs/utils"
type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata" type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata"
@Entity() @Entity()
@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" }) @Unique({
properties: ["provider", "scope", "entity_id"],
name: "IDX_auth_user_provider_scope_entity_id",
})
export default class AuthUser { export default class AuthUser {
[OptionalProps]: OptionalFields [OptionalProps]: OptionalFields
@@ -34,14 +37,17 @@ export default class AuthUser {
}) })
provider: AuthProvider provider: AuthProvider
@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", nullable: true }) @Property({ columnType: "jsonb" })
app_metadata: Record<string, unknown> | null app_metadata: Record<string, unknown> = {}
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
provider_metadata: Record<string, unknown> | null provider_metadata: Record<string, unknown> | null = null
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
+47 -5
View File
@@ -1,4 +1,8 @@
import { AbstractAuthModuleProvider, isString } from "@medusajs/utils" import {
AbstractAuthModuleProvider,
MedusaError,
isString,
} from "@medusajs/utils"
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import { AuthUserService } from "@services" import { AuthUserService } from "@services"
@@ -16,6 +20,17 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider {
this.authUserSerivce_ = authUserService this.authUserSerivce_ = authUserService
} }
private getHashConfig(scope: string) {
const scopeConfig = this.scopes_[scope].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( async authenticate(
userData: AuthenticationInput userData: AuthenticationInput
): Promise<AuthenticationResponse> { ): Promise<AuthenticationResponse> {
@@ -34,11 +49,38 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider {
error: "Email should be a string", error: "Email should be a string",
} }
} }
let authUser
const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( try {
email, authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
EmailPasswordProvider.PROVIDER email,
) EmailPasswordProvider.PROVIDER
)
} catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) {
const password_hash = await Scrypt.kdf(
password,
this.getHashConfig(userData.authScope)
)
const [createdAuthUser] = await this.authUserSerivce_.create([
{
entity_id: email,
provider: EmailPasswordProvider.PROVIDER,
scope: userData.authScope,
provider_metadata: {
password: password_hash.toString("base64"),
},
},
])
return {
success: true,
authUser: JSON.parse(JSON.stringify(createdAuthUser)),
}
}
return { success: false, error: error.message }
}
const password_hash = authUser.provider_metadata?.password const password_hash = authUser.provider_metadata?.password
+16 -22
View File
@@ -1,7 +1,4 @@
import { import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
AbstractAuthModuleProvider,
MedusaError,
} from "@medusajs/utils"
import { import {
AuthProviderScope, AuthProviderScope,
AuthenticationInput, AuthenticationInput,
@@ -9,6 +6,7 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { AuthProviderService, AuthUserService } from "@services" import { AuthProviderService, AuthUserService } 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"
@@ -78,7 +76,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
const code = req.query?.code ?? req.body?.code const code = req.query?.code ?? req.body?.code
return await this.validateCallbackToken(code, req.scope, config) return await this.validateCallbackToken(code, req.authScope, config)
} }
// abstractable // abstractable
@@ -97,14 +95,15 @@ class GoogleProvider extends AbstractAuthModuleProvider {
) )
} catch (error) { } catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) { if (error.type === MedusaError.Types.NOT_FOUND) {
authUser = await this.authUserSerivce_.create([ const [createdAuthUser] = await this.authUserSerivce_.create([
{ {
entity_id, entity_id,
provider_id: GoogleProvider.PROVIDER, provider: GoogleProvider.PROVIDER,
user_metadata: jwtData!.payload, user_metadata: jwtData!.payload,
app_metadata: { scope }, scope,
}, },
]) ])
authUser = createdAuthUser
} else { } else {
return { success: false, error: error.message } return { success: false, error: error.message }
} }
@@ -135,24 +134,20 @@ class GoogleProvider extends AbstractAuthModuleProvider {
} }
} }
private getConfigFromScope(config: AuthProviderScope): ProviderConfig { private getConfigFromScope(
const providerConfig: Partial<ProviderConfig> = {} config: AuthProviderScope & Partial<ProviderConfig>
): ProviderConfig {
const providerConfig: Partial<ProviderConfig> = { ...config }
if (config.clientId) { if (!providerConfig.clientID) {
providerConfig.clientID = config.clientId
} else {
throw new Error("Google clientID is required") throw new Error("Google clientID is required")
} }
if (config.clientSecret) { if (!providerConfig.clientSecret) {
providerConfig.clientSecret = config.clientSecret
} else {
throw new Error("Google clientSecret is required") throw new Error("Google clientSecret is required")
} }
if (config.callbackURL) { if (!providerConfig.callbackURL) {
providerConfig.callbackURL = config.callbackUrl
} else {
throw new Error("Google callbackUrl is required") throw new Error("Google callbackUrl is required")
} }
@@ -160,9 +155,8 @@ class GoogleProvider extends AbstractAuthModuleProvider {
} }
private originalURL(req: AuthenticationInput) { private originalURL(req: AuthenticationInput) {
const tls = req.connection.encrypted
const host = req.headers.host const host = req.headers.host
const protocol = tls ? "https" : "http" const protocol = req.protocol
const path = req.url || "" const path = req.url || ""
return protocol + "://" + host + path return protocol + "://" + host + path
@@ -173,7 +167,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
): Promise<ProviderConfig> { ): Promise<ProviderConfig> {
await this.authProviderService_.retrieve(GoogleProvider.PROVIDER) await this.authProviderService_.retrieve(GoogleProvider.PROVIDER)
const scopeConfig = this.scopes_[req.scope] const scopeConfig = this.scopes_[req.authScope]
const config = this.getConfigFromScope(scopeConfig) const config = this.getConfigFromScope(scopeConfig)
+2 -2
View File
@@ -395,7 +395,7 @@ export default class AuthModuleService<
protected getRegisteredAuthenticationProvider( protected getRegisteredAuthenticationProvider(
provider: string, provider: string,
{ scope }: AuthenticationInput { authScope }: AuthenticationInput
): AbstractAuthModuleProvider { ): AbstractAuthModuleProvider {
let containerProvider: AbstractAuthModuleProvider let containerProvider: AbstractAuthModuleProvider
try { try {
@@ -407,7 +407,7 @@ export default class AuthModuleService<
) )
} }
containerProvider.validateScope(scope) containerProvider.validateScope(authScope)
return containerProvider return containerProvider
} }
@@ -1,7 +1,7 @@
export type AuthProviderDTO = { export type AuthProviderDTO = {
provider: string provider: string
name: string name: string
domain: ProviderDomain scope: string
is_active: boolean is_active: boolean
config: Record<string, unknown> config: Record<string, unknown>
} }
@@ -9,7 +9,7 @@ export type AuthProviderDTO = {
export type CreateAuthProviderDTO = { export type CreateAuthProviderDTO = {
provider: string provider: string
name: string name: string
domain?: ProviderDomain scope?: string
is_active?: boolean is_active?: boolean
config?: Record<string, unknown> config?: Record<string, unknown>
} }
@@ -17,15 +17,8 @@ export type CreateAuthProviderDTO = {
export type UpdateAuthProviderDTO = { export type UpdateAuthProviderDTO = {
provider: string provider: string
name?: string name?: string
domain?: ProviderDomain
is_active?: boolean is_active?: boolean
config?: Record<string, unknown> config?: Record<string, unknown>
} }
export enum ProviderDomain {
ALL = "all",
STORE = "store",
ADMIN = "admin",
}
export type FilterableAuthProviderProps = {} export type FilterableAuthProviderProps = {}
@@ -4,6 +4,7 @@ export type AuthUserDTO = {
id: string id: string
provider_id: string provider_id: string
entity_id: string entity_id: string
scope: string
provider: AuthProviderDTO provider: AuthProviderDTO
provider_metadata?: Record<string, unknown> provider_metadata?: Record<string, unknown>
user_metadata: Record<string, unknown> user_metadata: Record<string, unknown>
@@ -12,7 +13,8 @@ export type AuthUserDTO = {
export type CreateAuthUserDTO = { export type CreateAuthUserDTO = {
entity_id: string entity_id: string
provider_id: string provider: string
scope: string
provider_metadata?: Record<string, unknown> provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown> user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown> app_metadata?: Record<string, unknown>
+1 -1
View File
@@ -55,7 +55,7 @@
"@mikro-orm/migrations": "5.9.7", "@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7", "@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0", "awilix": "^8.0.0",
"dotenv": "^16.1.4", "dotenv": "16.3.1",
"knex": "2.4.2" "knex": "2.4.2"
} }
} }
@@ -0,0 +1,46 @@
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { MedusaError } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, authProvider } = req.params
const service: IAuthModuleService = req.scope.resolve(
ModuleRegistrationName.AUTH
)
const authData = {
url: req.url,
headers: req.headers,
query: req.query,
body: req.body,
authScope: scope,
protocol: req.protocol,
} as AuthenticationInput
const authResult = await service.validateCallback(authProvider, authData)
const { success, error, authUser, location } = authResult
if (location) {
res.redirect(location)
return
}
if (success) {
req.session.auth_user = authUser
req.session.scope = authUser.scope
return res.status(200).json({ authUser })
}
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
error || "Authentication failed"
)
}
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
await GET(req, res)
}
@@ -0,0 +1,46 @@
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { MedusaError } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { scope, authProvider } = req.params
const service: IAuthModuleService = req.scope.resolve(
ModuleRegistrationName.AUTH
)
const authData = {
url: req.url,
headers: req.headers,
query: req.query,
body: req.body,
authScope: scope,
protocol: req.protocol,
} as AuthenticationInput
const authResult = await service.authenticate(authProvider, authData)
const { success, error, authUser, location } = authResult
if (location) {
res.redirect(location)
return
}
if (success) {
req.session.auth_user = authUser
req.session.scope = authUser.scope
return res.status(200).json({ authUser })
}
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
error || "Authentication failed"
)
}
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
await GET(req, res)
}
@@ -1,8 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing" import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => { export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const id = req.auth_user!.app_metadata.customer_id const id = req.auth_user!.app_metadata?.customer_id
const customerModule = req.scope.resolve(ModuleRegistrationName.CUSTOMER) const customerModule = req.scope.resolve(ModuleRegistrationName.CUSTOMER)
@@ -7,9 +7,10 @@ import {
StorePostCustomersMeAddressesAddressReq, StorePostCustomersMeAddressesAddressReq,
StoreGetCustomersMeAddressesParams, StoreGetCustomersMeAddressesParams,
} from "./validators" } from "./validators"
import authenticate from "../../../utils/authenticate-middleware"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { authenticate } from "../../../utils/authenticate-middleware"
export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [ export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [
{ {
method: "ALL", method: "ALL",
@@ -17,7 +17,7 @@ export const defaultStoreCustomersFields: (keyof CustomerDTO)[] = [
] ]
export const retrieveTransformQueryConfig = { export const retrieveTransformQueryConfig = {
defaultFields: defaultStoreCustomersFields, defaultFields: defaultStoreCustomersFields as string[],
defaultRelations: defaultStoreCustomersRelations, defaultRelations: defaultStoreCustomersRelations,
allowedRelations: allowedStoreCustomersRelations, allowedRelations: allowedStoreCustomersRelations,
isList: false, isList: false,
@@ -1,8 +1,30 @@
import { MedusaRequest, MedusaResponse } from "../../../types/routing" import { MedusaRequest, MedusaResponse } from "../../../types/routing"
import { createCustomerAccountWorkflow } from "@medusajs/core-flows"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { CreateCustomerDTO } from "@medusajs/types" import { CreateCustomerDTO } from "@medusajs/types"
import { createCustomerAccountWorkflow } from "@medusajs/core-flows"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => { export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
if (req.auth_user?.app_metadata?.customer_id) {
const remoteQuery = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const query = remoteQueryObjectFromString({
entryPoint: "customer",
variables: { id: req.auth_user.app_metadata.customer_id },
fields: [],
})
const [customer] = await remoteQuery(query)
res.status(200).json({ customer })
return
}
const createCustomers = createCustomerAccountWorkflow(req.scope) const createCustomers = createCustomerAccountWorkflow(req.scope)
const customersData = req.validatedBody as CreateCustomerDTO const customersData = req.validatedBody as CreateCustomerDTO
@@ -10,5 +32,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
input: { customersData, authUserId: req.auth_user!.id }, input: { customersData, authUserId: req.auth_user!.id },
}) })
// Set customer_id on session user if we are in session
if (req.session.auth_user) {
req.session.auth_user.app_metadata.customer_id = result.id
}
res.status(200).json({ customer: result }) res.status(200).json({ customer: result })
} }
+3 -1
View File
@@ -1,11 +1,13 @@
import type { Customer, User } from "../models"
import type { NextFunction, Request, Response } from "express" import type { NextFunction, Request, Response } from "express"
import type { Customer, User } from "../models" import { AuthUserDTO } from "@medusajs/types"
import type { MedusaContainer } from "./global" import type { MedusaContainer } from "./global"
export interface MedusaRequest extends Request { export interface MedusaRequest extends Request {
user?: (User | Customer) & { customer_id?: string; userId?: string } user?: (User | Customer) & { customer_id?: string; userId?: string }
scope: MedusaContainer scope: MedusaContainer
session?: any
requestId?: string requestId?: string
auth_user?: { id: string; app_metadata: Record<string, any>; scope: string } auth_user?: { id: string; app_metadata: Record<string, any>; scope: string }
} }
@@ -1,22 +1,20 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AuthUserDTO, IAuthModuleService } from "@medusajs/types" import { AuthUserDTO, IAuthModuleService } from "@medusajs/types"
import { NextFunction, RequestHandler } from "express"
import { MedusaRequest, MedusaResponse } from "../types/routing" import { MedusaRequest, MedusaResponse } from "../types/routing"
import { NextFunction, RequestHandler } from "express"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
const SESSION_AUTH = "session" const SESSION_AUTH = "session"
const BEARER_AUTH = "bearer" const BEARER_AUTH = "bearer"
type MedusaSession = { type MedusaSession = {
auth: { auth_user: AuthUserDTO
[authScope: string]: { scope: string
user_id: string
}
}
} }
type AuthType = "session" | "bearer" type AuthType = "session" | "bearer"
export default ( export const authenticate = (
authScope: string, authScope: string,
authType: AuthType | AuthType[], authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean } = {} options: { allowUnauthenticated?: boolean } = {}
@@ -36,19 +34,18 @@ export default (
let authUser: AuthUserDTO | null = null let authUser: AuthUserDTO | null = null
if (authTypes.includes(SESSION_AUTH)) { if (authTypes.includes(SESSION_AUTH)) {
if (session.auth && session.auth[authScope]) { if (session.auth_user && session.scope === authScope) {
authUser = await authModule authUser = session.auth_user
.retrieveAuthUser(session.auth[authScope].user_id)
.catch(() => null)
} }
} }
if (authTypes.includes(BEARER_AUTH)) { if (!authUser && authTypes.includes(BEARER_AUTH)) {
const authHeader = req.headers.authorization const authHeader = req.headers.authorization
if (authHeader) { if (authHeader) {
const re = /(\S+)\s+(\S+)/ const re = /(\S+)\s+(\S+)/
const matches = authHeader.match(re) const matches = authHeader.match(re)
// TODO: figure out how to obtain token (and store correct data in token)
if (matches) { if (matches) {
const tokenType = matches[1] const tokenType = matches[1]
const token = matches[2] const token = matches[2]
@@ -3,7 +3,7 @@ import { BaseFilterable } from "../../dal"
export type AuthProviderDTO = { export type AuthProviderDTO = {
provider: string provider: string
name: string name: string
domain: ProviderDomain scope?: string
is_active: boolean is_active: boolean
config: Record<string, unknown> | null config: Record<string, unknown> | null
} }
@@ -11,29 +11,22 @@ export type AuthProviderDTO = {
export type CreateAuthProviderDTO = { export type CreateAuthProviderDTO = {
provider: string provider: string
name: string name: string
domain?: ProviderDomain scope?: string
is_active?: boolean is_active?: boolean
config?: Record<string, unknown> config?: Record<string, unknown>
} }
export type UpdateAuthProviderDTO = { export type UpdateAuthProviderDTO = {
provider: string provider: string
name?: string name?: string
domain?: ProviderDomain
is_active?: boolean is_active?: boolean
config?: Record<string, unknown> config?: Record<string, unknown>
} }
export enum ProviderDomain {
ALL = "all",
STORE = "store",
ADMIN = "admin",
}
export interface FilterableAuthProviderProps export interface FilterableAuthProviderProps
extends BaseFilterable<FilterableAuthProviderProps> { extends BaseFilterable<FilterableAuthProviderProps> {
provider?: string[] provider?: string[]
is_active?: boolean is_active?: boolean
domain?: ProviderDomain[] scope?: string[]
name?: string[] name?: string[]
} }
+4 -2
View File
@@ -1,10 +1,11 @@
import { BaseFilterable } from "../../dal"
import { AuthProviderDTO } from "./auth-provider" import { AuthProviderDTO } from "./auth-provider"
import { BaseFilterable } from "../../dal"
export type AuthUserDTO = { export type AuthUserDTO = {
id: string id: string
provider_id: string provider_id: string
entity_id: string entity_id: string
scope: string
provider: AuthProviderDTO provider: AuthProviderDTO
provider_metadata?: Record<string, unknown> provider_metadata?: Record<string, unknown>
user_metadata: Record<string, unknown> user_metadata: Record<string, unknown>
@@ -12,8 +13,9 @@ export type AuthUserDTO = {
} }
export type CreateAuthUserDTO = { export type CreateAuthUserDTO = {
provider_id: string provider: string
entity_id: string entity_id: string
scope: string
provider_metadata?: Record<string, unknown> provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown> user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown> app_metadata?: Record<string, unknown>
+3 -3
View File
@@ -10,13 +10,13 @@ export type AuthModuleProviderConfig = {
scopes: Record<string, AuthProviderScope> scopes: Record<string, AuthProviderScope>
} }
export type AuthProviderScope = { domain?: string } & Record<string, string> export type AuthProviderScope = Record<string, unknown>
export type AuthenticationInput = { export type AuthenticationInput = {
connection: { encrypted: boolean }
url: string url: string
headers: Record<string, string> headers: Record<string, string>
query: Record<string, string> query: Record<string, string>
body: Record<string, string> body: Record<string, string>
scope: string authScope: string
protocol: string
} }
+2 -2
View File
@@ -7902,7 +7902,7 @@ __metadata:
"@mikro-orm/postgresql": 5.9.7 "@mikro-orm/postgresql": 5.9.7
awilix: ^8.0.0 awilix: ^8.0.0
cross-env: ^5.2.1 cross-env: ^5.2.1
dotenv: ^16.1.4 dotenv: 16.3.1
jest: ^29.6.3 jest: ^29.6.3
jsonwebtoken: ^9.0.2 jsonwebtoken: ^9.0.2
knex: 2.4.2 knex: 2.4.2
@@ -8022,7 +8022,7 @@ __metadata:
"@mikro-orm/postgresql": 5.9.7 "@mikro-orm/postgresql": 5.9.7
awilix: ^8.0.0 awilix: ^8.0.0
cross-env: ^5.2.1 cross-env: ^5.2.1
dotenv: ^16.1.4 dotenv: 16.3.1
jest: ^29.6.3 jest: ^29.6.3
knex: 2.4.2 knex: 2.4.2
medusa-test-utils: ^1.1.40 medusa-test-utils: ^1.1.40