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:
co-authored by
Sebastian Rindom
parent
061c449179
commit
9fda6a6824
+16
-12
@@ -24,20 +24,14 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"domain": {
|
||||
"name": "domain",
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'all'",
|
||||
"enumItems": [
|
||||
"all",
|
||||
"store",
|
||||
"admin"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
@@ -104,6 +98,15 @@
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"user_metadata": {
|
||||
"name": "user_metadata",
|
||||
"type": "jsonb",
|
||||
@@ -119,7 +122,7 @@
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"provider_metadata": {
|
||||
@@ -136,9 +139,10 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_auth_user_provider_entity_id",
|
||||
"keyName": "IDX_auth_user_provider_scope_entity_id",
|
||||
"columnNames": [
|
||||
"provider_id",
|
||||
"scope",
|
||||
"entity_id"
|
||||
],
|
||||
"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;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,8 +20,8 @@ export default class AuthProvider {
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL })
|
||||
domain: ProviderDomain = ProviderDomain.ALL
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
scope: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
config: Record<string, unknown> | null = null
|
||||
|
||||
@@ -17,7 +17,10 @@ import { generateEntityId } from "@medusajs/utils"
|
||||
type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata"
|
||||
|
||||
@Entity()
|
||||
@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" })
|
||||
@Unique({
|
||||
properties: ["provider", "scope", "entity_id"],
|
||||
name: "IDX_auth_user_provider_scope_entity_id",
|
||||
})
|
||||
export default class AuthUser {
|
||||
[OptionalProps]: OptionalFields
|
||||
|
||||
@@ -34,14 +37,17 @@ export default class AuthUser {
|
||||
})
|
||||
provider: AuthProvider
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
scope: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
user_metadata: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
app_metadata: Record<string, unknown> | null
|
||||
@Property({ columnType: "jsonb" })
|
||||
app_metadata: Record<string, unknown> = {}
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
provider_metadata: Record<string, unknown> | null
|
||||
provider_metadata: Record<string, unknown> | null = null
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AbstractAuthModuleProvider, isString } from "@medusajs/utils"
|
||||
import {
|
||||
AbstractAuthModuleProvider,
|
||||
MedusaError,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
|
||||
|
||||
import { AuthUserService } from "@services"
|
||||
@@ -16,6 +20,17 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider {
|
||||
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(
|
||||
userData: AuthenticationInput
|
||||
): Promise<AuthenticationResponse> {
|
||||
@@ -34,11 +49,38 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider {
|
||||
error: "Email should be a string",
|
||||
}
|
||||
}
|
||||
let authUser
|
||||
|
||||
const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
|
||||
email,
|
||||
EmailPasswordProvider.PROVIDER
|
||||
)
|
||||
try {
|
||||
authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
|
||||
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
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AbstractAuthModuleProvider,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
AuthProviderScope,
|
||||
AuthenticationInput,
|
||||
@@ -9,6 +6,7 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import { AuthProviderService, AuthUserService } from "@services"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
|
||||
import { AuthorizationCode } from "simple-oauth2"
|
||||
import url from "url"
|
||||
|
||||
@@ -78,7 +76,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
|
||||
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
|
||||
@@ -97,14 +95,15 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
)
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
authUser = await this.authUserSerivce_.create([
|
||||
const [createdAuthUser] = await this.authUserSerivce_.create([
|
||||
{
|
||||
entity_id,
|
||||
provider_id: GoogleProvider.PROVIDER,
|
||||
provider: GoogleProvider.PROVIDER,
|
||||
user_metadata: jwtData!.payload,
|
||||
app_metadata: { scope },
|
||||
scope,
|
||||
},
|
||||
])
|
||||
authUser = createdAuthUser
|
||||
} else {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
@@ -135,24 +134,20 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private getConfigFromScope(config: AuthProviderScope): ProviderConfig {
|
||||
const providerConfig: Partial<ProviderConfig> = {}
|
||||
private getConfigFromScope(
|
||||
config: AuthProviderScope & Partial<ProviderConfig>
|
||||
): ProviderConfig {
|
||||
const providerConfig: Partial<ProviderConfig> = { ...config }
|
||||
|
||||
if (config.clientId) {
|
||||
providerConfig.clientID = config.clientId
|
||||
} else {
|
||||
if (!providerConfig.clientID) {
|
||||
throw new Error("Google clientID is required")
|
||||
}
|
||||
|
||||
if (config.clientSecret) {
|
||||
providerConfig.clientSecret = config.clientSecret
|
||||
} else {
|
||||
if (!providerConfig.clientSecret) {
|
||||
throw new Error("Google clientSecret is required")
|
||||
}
|
||||
|
||||
if (config.callbackURL) {
|
||||
providerConfig.callbackURL = config.callbackUrl
|
||||
} else {
|
||||
if (!providerConfig.callbackURL) {
|
||||
throw new Error("Google callbackUrl is required")
|
||||
}
|
||||
|
||||
@@ -160,9 +155,8 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
}
|
||||
|
||||
private originalURL(req: AuthenticationInput) {
|
||||
const tls = req.connection.encrypted
|
||||
const host = req.headers.host
|
||||
const protocol = tls ? "https" : "http"
|
||||
const protocol = req.protocol
|
||||
const path = req.url || ""
|
||||
|
||||
return protocol + "://" + host + path
|
||||
@@ -173,7 +167,7 @@ class GoogleProvider extends AbstractAuthModuleProvider {
|
||||
): Promise<ProviderConfig> {
|
||||
await this.authProviderService_.retrieve(GoogleProvider.PROVIDER)
|
||||
|
||||
const scopeConfig = this.scopes_[req.scope]
|
||||
const scopeConfig = this.scopes_[req.authScope]
|
||||
|
||||
const config = this.getConfigFromScope(scopeConfig)
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ export default class AuthModuleService<
|
||||
|
||||
protected getRegisteredAuthenticationProvider(
|
||||
provider: string,
|
||||
{ scope }: AuthenticationInput
|
||||
{ authScope }: AuthenticationInput
|
||||
): AbstractAuthModuleProvider {
|
||||
let containerProvider: AbstractAuthModuleProvider
|
||||
try {
|
||||
@@ -407,7 +407,7 @@ export default class AuthModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
containerProvider.validateScope(scope)
|
||||
containerProvider.validateScope(authScope)
|
||||
|
||||
return containerProvider
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type AuthProviderDTO = {
|
||||
provider: string
|
||||
name: string
|
||||
domain: ProviderDomain
|
||||
scope: string
|
||||
is_active: boolean
|
||||
config: Record<string, unknown>
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export type AuthProviderDTO = {
|
||||
export type CreateAuthProviderDTO = {
|
||||
provider: string
|
||||
name: string
|
||||
domain?: ProviderDomain
|
||||
scope?: string
|
||||
is_active?: boolean
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
@@ -17,15 +17,8 @@ export type CreateAuthProviderDTO = {
|
||||
export type UpdateAuthProviderDTO = {
|
||||
provider: string
|
||||
name?: string
|
||||
domain?: ProviderDomain
|
||||
is_active?: boolean
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export enum ProviderDomain {
|
||||
ALL = "all",
|
||||
STORE = "store",
|
||||
ADMIN = "admin",
|
||||
}
|
||||
|
||||
export type FilterableAuthProviderProps = {}
|
||||
|
||||
@@ -4,6 +4,7 @@ export type AuthUserDTO = {
|
||||
id: string
|
||||
provider_id: string
|
||||
entity_id: string
|
||||
scope: string
|
||||
provider: AuthProviderDTO
|
||||
provider_metadata?: Record<string, unknown>
|
||||
user_metadata: Record<string, unknown>
|
||||
@@ -12,7 +13,8 @@ export type AuthUserDTO = {
|
||||
|
||||
export type CreateAuthUserDTO = {
|
||||
entity_id: string
|
||||
provider_id: string
|
||||
provider: string
|
||||
scope: string
|
||||
provider_metadata?: Record<string, unknown>
|
||||
user_metadata?: Record<string, unknown>
|
||||
app_metadata?: Record<string, unknown>
|
||||
|
||||
Reference in New Issue
Block a user