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 Korsholm
2024-02-02 10:45:32 +00:00
committed by GitHub
co-authored by Sebastian Rindom
parent 061c449179
commit 9fda6a6824
31 changed files with 302 additions and 146 deletions
+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 { 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
+16 -22
View File
@@ -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)