feat: Add github authentication provider (#8980)

* feat: Add github authentication provider

* feat: Change callback to always return a token, expect callbackUrl to point to FE

* fix: Return login redirect URLas a 200 response
This commit is contained in:
Stevche Radevski
2024-09-04 13:14:00 +02:00
committed by GitHub
parent fb832072a4
commit af4f8811bd
23 changed files with 717 additions and 46 deletions
+8
View File
@@ -0,0 +1,8 @@
---
"@medusajs/auth-github": patch
"@medusajs/auth-google": patch
"@medusajs/auth": patch
"@medusajs/types": patch
---
Add github auth provider
+38 -4
View File
@@ -30,15 +30,49 @@ export class Auth {
} }
login = async ( login = async (
actor: string,
method: string,
payload: HttpTypes.AdminSignInWithEmailPassword | Record<string, unknown>
) => {
// There will either be token or location returned from the backend.
const { token, location } = await this.client.fetch<{
token?: string
location?: string
}>(`/auth/${actor}/${method}`, {
method: "POST",
body: payload,
})
// In the case of an oauth login, we return the redirect location to the caller.
// They can decide if they do an immediate redirect or put it in an <a> tag.
if (location) {
return { location }
}
// By default we just set the token in memory, if configured to use sessions we convert it into session storage instead.
if (this.config?.auth?.type === "session") {
await this.client.fetch("/auth/session", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
})
} else {
this.client.setToken(token as string)
}
return token
}
// The callback expects all query parameters from the Oauth callback to be passed to the backend, and the provider is in charge of parsing and validating them
callback = async (
actor: "customer" | "user", actor: "customer" | "user",
method: "emailpass", method: "emailpass",
payload: HttpTypes.AdminSignInWithEmailPassword query?: Record<string, unknown>
) => { ) => {
const { token } = await this.client.fetch<{ token: string }>( const { token } = await this.client.fetch<{ token: string }>(
`/auth/${actor}/${method}`, `/auth/${actor}/${method}/callback`,
{ {
method: "POST", method: "GET",
body: payload, query,
} }
) )
@@ -33,15 +33,6 @@ export type AuthenticationResponse = {
* specified location. * specified location.
*/ */
location?: string location?: string
/**
* Some authentication providers support redirecting to a specified URL on
* success. In those cases, the URL to redirect to is set in this field.
*
* So, if `success` is true, there's no `location` set, and this field
* is set, you can redirect to this URL.
*/
successRedirectUrl?: string
} }
/** /**
+7
View File
@@ -13,6 +13,13 @@ export interface AuthIdentityProviderService {
provider_metadata?: Record<string, unknown> provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown> user_metadata?: Record<string, unknown>
}) => Promise<AuthIdentityDTO> }) => Promise<AuthIdentityDTO>
update: (
entity_id: string,
data: {
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
}
) => Promise<AuthIdentityDTO>
} }
/** /**
@@ -0,0 +1,5 @@
export interface GithubAuthProviderOptions {
clientId: string
clientSecret: string
callbackUrl: string
}
@@ -1,6 +1,5 @@
export interface GoogleAuthProviderOptions { export interface GoogleAuthProviderOptions {
clientID: string clientId: string
clientSecret: string clientSecret: string
callbackURL: string callbackUrl: string
successRedirectUrl?: string
} }
@@ -1,2 +1,3 @@
export * from "./emailpass" export * from "./emailpass"
export * from "./google" export * from "./google"
export * from "./github"
+3 -3
View File
@@ -46,12 +46,12 @@ export interface IAuthModuleService extends IModuleService {
*/ */
authenticate( authenticate(
provider: string, provider: string,
providerData: AuthenticationInput, providerData: AuthenticationInput
): Promise<AuthenticationResponse> ): Promise<AuthenticationResponse>
register( register(
provider: string, provider: string,
providerData: AuthenticationInput, providerData: AuthenticationInput
): Promise<AuthenticationResponse> ): Promise<AuthenticationResponse>
/** /**
@@ -76,7 +76,7 @@ export interface IAuthModuleService extends IModuleService {
* `req` is an instance of the `MedusaRequest` object: * `req` is an instance of the `MedusaRequest` object:
* *
* ```ts * ```ts
* const { success, authIdentity, error, successRedirectUrl } = * const { success, authIdentity, error } =
* await authModuleService.validateCallback("google", { * await authModuleService.validateCallback("google", {
* url: req.url, * url: req.url,
* headers: req.headers, * headers: req.headers,
@@ -41,8 +41,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
protocol: req.protocol, protocol: req.protocol,
} as AuthenticationInput } as AuthenticationInput
const { success, error, authIdentity, successRedirectUrl } = const { success, error, authIdentity } = await service.validateCallback(
await service.validateCallback(auth_provider, authData) auth_provider,
authData
)
const entityIdKey = `${actor_type}_id` const entityIdKey = `${actor_type}_id`
const entityId = authIdentity?.app_metadata?.[entityIdKey] as const entityId = authIdentity?.app_metadata?.[entityIdKey] as
@@ -71,13 +73,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
} }
) )
if (successRedirectUrl) {
const url = new URL(successRedirectUrl!)
url.searchParams.append("access_token", token)
return res.redirect(url.toString())
}
return res.json({ token }) return res.json({ token })
} }
@@ -47,8 +47,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
) )
if (location) { if (location) {
res.redirect(location) return res.status(200).json({ location })
return
} }
if (success) { if (success) {
@@ -282,6 +282,74 @@ export default class AuthModuleService
createdAuthIdentity createdAuthIdentity
) )
}, },
update: async (
entity_id: string,
data: {
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
}
) => {
const authIdentities = await this.authIdentityService_.list(
{
provider_identities: {
entity_id,
provider,
},
},
{
relations: ["provider_identities"],
}
)
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}"`
)
}
const providerIdentityData = authIdentities[0].provider_identities.find(
(pi) => pi.provider === provider
)
if (!providerIdentityData) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProviderIdentity with entity_id "${entity_id}" not found`
)
}
const updatedProviderIdentity =
await this.providerIdentityService_.update({
id: providerIdentityData.id,
...data,
})
const serializedResponse =
await this.baseRepository_.serialize<AuthTypes.AuthIdentityDTO>(
authIdentities[0]
)
const serializedProviderIdentity =
await this.baseRepository_.serialize<AuthTypes.ProviderIdentityDTO>(
updatedProviderIdentity
)
serializedResponse.provider_identities = [
...(serializedResponse.provider_identities?.filter(
(p) => p.provider !== provider
) ?? []),
serializedProviderIdentity,
]
return serializedResponse
},
} }
} }
} }
@@ -0,0 +1,11 @@
## Testing
In order to manually test the flow, you can do the following:
1. Register a Github App - https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app
2. Go to the app, fetch the clientId and create a new clientSecret
3. Replace the values in the test with your credentials
4. Remove the `server.listen()` call
5. Run the tests, get the `location` value from the `authenticate` test, open the browser
6. Once redirected, copy the `code` param from the URL, and add it in one of the `callback` success tests
7. Once you run the tests, you should get back an access token and so on.
@@ -0,0 +1,246 @@
import { generateJwtToken, MedusaError } from "@medusajs/utils"
import { GithubAuthService } from "../../src/services/github"
import { http, HttpResponse } from "msw"
import { setupServer } from "msw/node"
jest.setTimeout(100000)
const sampleIdPayload = {
login: "octocat",
id: 1,
node_id: "MDQ6VXNlcjE=",
avatar_url: "https://github.com/images/error/octocat_happy.gif",
gravatar_id: "",
url: "https://api.github.com/users/octocat",
name: "monalisa octocat",
company: "GitHub",
location: "San Francisco",
email: "octocat@github.com",
two_factor_authentication: true,
}
const baseUrl = "https://someurl.com"
// This is just a network-layer mocking, it doesn't start an actual server
const server = setupServer(
http.post(
"https://github.com/login/oauth/access_token",
async ({ request, params, cookies }) => {
const url = request.url
if (
url ===
"https://github.com/login/oauth/access_token?client_id=test&client_secret=test&code=invalid-code&redirect_uri=https%3A%2F%2Fsomeurl.com%2Fauth%2Fgithub%2Fcallback"
) {
return new HttpResponse(null, {
status: 401,
statusText: "Unauthorized",
})
}
if (
url ===
"https://github.com/login/oauth/access_token?client_id=test&client_secret=test&code=valid-code&redirect_uri=https%3A%2F%2Fsomeurl.com%2Fauth%2Fgithub%2Fcallback"
) {
return new HttpResponse(
JSON.stringify({
access_token: "test",
expires_in: 3600,
refresh_token: "test",
refresh_token_expires_in: 7200,
})
)
}
}
),
http.get(
"https://api.github.com/user",
async ({ request, params, cookies }) => {
return new HttpResponse(JSON.stringify(sampleIdPayload), {
status: 200,
statusText: "OK",
})
}
),
http.all("*", ({ request, params, cookies }) => {
return new HttpResponse(null, {
status: 404,
statusText: "Not Found",
})
})
)
describe("Github auth provider", () => {
let githubService: GithubAuthService
beforeAll(() => {
githubService = new GithubAuthService(
{
logger: console as any,
},
{
clientId: "test",
clientSecret: "test",
callbackUrl: `${baseUrl}/auth/github/callback`,
}
)
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.restoreAllMocks()
})
afterAll(() => server.close())
it("throw an error if required options are not passed", async () => {
let msg = ""
try {
GithubAuthService.validateOptions({
clientId: "test",
clientSecret: "test",
} as any)
} catch (e) {
msg = e.message
}
expect(msg).toEqual("Github callbackUrl is required")
})
it("returns a redirect URL on authenticate", async () => {
const res = await githubService.authenticate({})
expect(res).toEqual({
success: true,
location:
"https://github.com/login/oauth/authorize?redirect_uri=https%3A%2F%2Fsomeurl.com%2Fauth%2Fgithub%2Fcallback&client_id=test&response_type=code",
})
})
it("validate callback should return an error on empty code", async () => {
const res = await githubService.validateCallback(
{
query: {},
},
{} as any
)
expect(res).toEqual({
success: false,
error: "No code provided",
})
})
it("validate callback should return on a missing access token for code", async () => {
const res = await githubService.validateCallback(
{
query: {
code: "invalid-code",
},
},
{} as any
)
expect(res).toEqual({
success: false,
error: "Could not exchange token, 401, Unauthorized",
})
})
it("validate callback should return successfully on a correct code for a new user", async () => {
const authServiceSpies = {
retrieve: jest.fn().mockImplementation(() => {
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Not found")
}),
create: jest.fn().mockImplementation(() => {
return {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "github",
},
],
}
}),
update: jest.fn().mockImplementation(() => {
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Not found")
}),
}
const res = await githubService.validateCallback(
{
query: {
code: "valid-code",
},
},
authServiceSpies
)
expect(res).toEqual({
success: true,
authIdentity: {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "github",
},
],
},
})
})
it("validate callback should return successfully on a correct code for an existing user", async () => {
const authServiceSpies = {
retrieve: jest.fn().mockImplementation(() => {
return {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "github",
},
],
}
}),
create: jest.fn().mockImplementation(() => {
return {}
}),
update: jest.fn().mockImplementation(() => {
return {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "github",
provider_metadata: {
access_token: "test",
},
},
],
}
}),
}
const res = await githubService.validateCallback(
{
query: {
code: "valid-code",
},
},
authServiceSpies
)
expect(res).toEqual({
success: true,
authIdentity: {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "github",
provider_metadata: {
access_token: "test",
},
},
],
},
})
})
})
@@ -0,0 +1,5 @@
module.exports = {
transform: { "^.+\\.[jt]s?$": "@swc/jest" },
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}
@@ -0,0 +1,41 @@
{
"name": "@medusajs/auth-github",
"version": "0.0.1",
"description": "Github OAuth authentication provider for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/providers/auth-github"
},
"files": [
"dist"
],
"engines": {
"node": ">=20"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"test": "jest --passWithNoTests src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
"build": "rimraf dist && tsc --build",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/types": "^1.11.16",
"@types/simple-oauth2": "^5.0.7",
"cross-env": "^5.2.1",
"jest": "^29.7.0",
"msw": "^2.3.0",
"rimraf": "^5.0.1",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/utils": "^1.11.9"
},
"keywords": [
"medusa-provider",
"medusa-provider-auth-github"
]
}
@@ -0,0 +1,10 @@
import { ModuleProviderExports } from "@medusajs/types"
import { GithubAuthService } from "./services/github"
const services = [GithubAuthService]
const providerExport: ModuleProviderExports = {
services,
}
export default providerExport
@@ -0,0 +1,203 @@
import {
AuthenticationInput,
AuthenticationResponse,
AuthIdentityProviderService,
GithubAuthProviderOptions,
Logger,
} from "@medusajs/types"
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
type InjectedDependencies = {
logger: Logger
}
interface LocalServiceConfig extends GithubAuthProviderOptions {}
// TODO: Add state param that is stored in Redis, to prevent CSRF attacks
export class GithubAuthService extends AbstractAuthModuleProvider {
protected config_: LocalServiceConfig
protected logger_: Logger
static validateOptions(options: GithubAuthProviderOptions) {
if (!options.clientId) {
throw new Error("Github clientId is required")
}
if (!options.clientSecret) {
throw new Error("Github clientSecret is required")
}
if (!options.callbackUrl) {
throw new Error("Github callbackUrl is required")
}
}
constructor(
{ logger }: InjectedDependencies,
options: GithubAuthProviderOptions
) {
super({}, { provider: "github", displayName: "Github Authentication" })
this.config_ = options
this.logger_ = logger
}
async register(_): Promise<AuthenticationResponse> {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Github does not support registration. Use method `authenticate` instead."
)
}
async authenticate(
req: AuthenticationInput
): Promise<AuthenticationResponse> {
if (req.query?.error) {
return {
success: false,
error: `${req.query.error_description}, read more at: ${req.query.error_uri}`,
}
}
return this.getRedirect(this.config_)
}
async validateCallback(
req: AuthenticationInput,
authIdentityService: AuthIdentityProviderService
): Promise<AuthenticationResponse> {
if (req.query && req.query.error) {
return {
success: false,
error: `${req.query.error_description}, read more at: ${req.query.error_uri}`,
}
}
const code = req.query?.code ?? req.body?.code
if (!code) {
return { success: false, error: "No code provided" }
}
const params = `client_id=${this.config_.clientId}&client_secret=${
this.config_.clientSecret
}&code=${code}&redirect_uri=${encodeURIComponent(this.config_.callbackUrl)}`
const exchangeTokenUrl = new URL(
`https://github.com/login/oauth/access_token?${params}`
)
try {
const response = await fetch(exchangeTokenUrl.toString(), {
method: "POST",
headers: {
Accept: "application/json",
},
}).then((r) => {
if (!r.ok) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Could not exchange token, ${r.status}, ${r.statusText}`
)
}
return r.json()
})
const providerMetadata = {
access_token: response.access_token,
refresh_token: response.refresh_token,
// The response is in seconds
access_token_expires_at: new Date(
Date.now() + response.expires_in * 1000
).toISOString(),
refresh_token_expires_at: new Date(
Date.now() + response.refresh_token_expires_in * 1000
).toISOString(),
}
const { authIdentity, success } = await this.upsert_(
providerMetadata,
authIdentityService
)
return {
success,
authIdentity,
}
} catch (error) {
return { success: false, error: error.message }
}
}
async upsert_(
providerMetadata: {
access_token: string
refresh_token: string
access_token_expires_at: string
refresh_token_expires_at: string
},
authIdentityService: AuthIdentityProviderService
) {
if (!providerMetadata?.access_token) {
return { success: false, error: "No access token found" }
}
const user = await fetch("https://api.github.com/user", {
headers: {
Accept: "application/json",
Authorization: `Bearer ${providerMetadata.access_token}`,
},
}).then((r) => r.json())
const entity_id = user.id
const userMetadata = {
profile_url: user.url,
avatar: user.avatar_url,
email: user.email,
name: user.name,
company: user.company,
two_factor_authentication: user.two_factor_authentication ?? false,
}
let authIdentity
try {
// Update throws if auth identity not found
authIdentity = await authIdentityService.update(entity_id, {
provider_metadata: providerMetadata,
user_metadata: userMetadata,
})
} catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) {
const createdAuthIdentity = await authIdentityService.create({
entity_id,
user_metadata: userMetadata,
provider_metadata: providerMetadata,
})
authIdentity = createdAuthIdentity
} else {
return { success: false, error: error.message }
}
}
return {
success: true,
authIdentity,
}
}
private getRedirect({ clientId, callbackUrl }: LocalServiceConfig) {
const redirectUrlParam = `redirect_uri=${encodeURIComponent(callbackUrl)}`
const clientIdParam = `client_id=${clientId}`
const responseTypeParam = "response_type=code"
const authUrl = new URL(
`https://github.com/login/oauth/authorize?${[
redirectUrlParam,
clientIdParam,
responseTypeParam,
].join("&")}`
)
return { success: true, location: authUrl.toString() }
}
}
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"lib": ["es2021"],
"target": "es2021",
"outDir": "./dist",
"esModuleInterop": true,
"declarationMap": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"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"
]
}
@@ -3,7 +3,7 @@
In order to manually test the flow, you can do the following: In order to manually test the flow, you can do the following:
1. Register an app in `https://console.cloud.google.com/apis/credentials/consent` 1. Register an app in `https://console.cloud.google.com/apis/credentials/consent`
2. Generate clientID and clientSecret credentials in the console 2. Generate clientId and clientSecret credentials in the console
3. Replace the values in the test with your credentials 3. Replace the values in the test with your credentials
4. Remove the `server.listen()` call 4. Remove the `server.listen()` call
5. Run the tests, get the `location` value from the `authenticate` test, open the browser 5. Run the tests, get the `location` value from the `authenticate` test, open the browser
@@ -78,10 +78,9 @@ describe("Google auth provider", () => {
logger: console as any, logger: console as any,
}, },
{ {
clientID: "test", clientId: "test",
clientSecret: "test", clientSecret: "test",
successRedirectUrl: baseUrl, callbackUrl: `${baseUrl}/auth/google/callback`,
callbackURL: `${baseUrl}/auth/google/callback`,
} }
) )
@@ -99,7 +98,7 @@ describe("Google auth provider", () => {
let msg = "" let msg = ""
try { try {
GoogleAuthService.validateOptions({ GoogleAuthService.validateOptions({
clientID: "test", clientId: "test",
clientSecret: "test", clientSecret: "test",
} as any) } as any)
} catch (e) { } catch (e) {
@@ -162,6 +161,9 @@ describe("Google auth provider", () => {
], ],
} }
}), }),
update: jest.fn().mockImplementation(() => {
return {}
}),
} }
const res = await googleService.validateCallback( const res = await googleService.validateCallback(
@@ -175,7 +177,6 @@ describe("Google auth provider", () => {
expect(res).toEqual({ expect(res).toEqual({
success: true, success: true,
successRedirectUrl: baseUrl,
authIdentity: { authIdentity: {
provider_identities: [ provider_identities: [
{ {
@@ -202,6 +203,9 @@ describe("Google auth provider", () => {
create: jest.fn().mockImplementation(() => { create: jest.fn().mockImplementation(() => {
return {} return {}
}), }),
update: jest.fn().mockImplementation(() => {
return {}
}),
} }
const res = await googleService.validateCallback( const res = await googleService.validateCallback(
@@ -215,7 +219,6 @@ describe("Google auth provider", () => {
expect(res).toEqual({ expect(res).toEqual({
success: true, success: true,
successRedirectUrl: baseUrl,
authIdentity: { authIdentity: {
provider_identities: [ provider_identities: [
{ {
@@ -20,15 +20,15 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
protected logger_: Logger protected logger_: Logger
static validateOptions(options: GoogleAuthProviderOptions) { static validateOptions(options: GoogleAuthProviderOptions) {
if (!options.clientID) { if (!options.clientId) {
throw new Error("Google clientID is required") throw new Error("Google clientId is required")
} }
if (!options.clientSecret) { if (!options.clientSecret) {
throw new Error("Google clientSecret is required") throw new Error("Google clientSecret is required")
} }
if (!options.callbackURL) { if (!options.callbackUrl) {
throw new Error("Google callbackUrl is required") throw new Error("Google callbackUrl is required")
} }
} }
@@ -78,10 +78,10 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
return { success: false, error: "No code provided" } return { success: false, error: "No code provided" }
} }
const params = `client_id=${this.config_.clientID}&client_secret=${ const params = `client_id=${this.config_.clientId}&client_secret=${
this.config_.clientSecret this.config_.clientSecret
}&code=${code}&redirect_uri=${encodeURIComponent( }&code=${code}&redirect_uri=${encodeURIComponent(
this.config_.callbackURL this.config_.callbackUrl
)}&grant_type=authorization_code` )}&grant_type=authorization_code`
const exchangeTokenUrl = new URL( const exchangeTokenUrl = new URL(
`https://oauth2.googleapis.com/token?${params}` `https://oauth2.googleapis.com/token?${params}`
@@ -109,7 +109,6 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
return { return {
success, success,
authIdentity, authIdentity,
successRedirectUrl: this.config_.successRedirectUrl,
} }
} catch (error) { } catch (error) {
return { success: false, error: error.message } return { success: false, error: error.message }
@@ -169,9 +168,9 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
} }
} }
private getRedirect({ clientID, callbackURL }: LocalServiceConfig) { private getRedirect({ clientId, callbackUrl }: LocalServiceConfig) {
const redirectUrlParam = `redirect_uri=${encodeURIComponent(callbackURL)}` const redirectUrlParam = `redirect_uri=${encodeURIComponent(callbackUrl)}`
const clientIdParam = `client_id=${clientID}` const clientIdParam = `client_id=${clientId}`
const responseTypeParam = "response_type=code" const responseTypeParam = "response_type=code"
const scopeParam = "scope=email+profile+openid" const scopeParam = "scope=email+profile+openid"
+15
View File
@@ -4364,6 +4364,21 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/auth-github@workspace:packages/modules/providers/auth-github":
version: 0.0.0-use.local
resolution: "@medusajs/auth-github@workspace:packages/modules/providers/auth-github"
dependencies:
"@medusajs/types": ^1.11.16
"@medusajs/utils": ^1.11.9
"@types/simple-oauth2": ^5.0.7
cross-env: ^5.2.1
jest: ^29.7.0
msw: ^2.3.0
rimraf: ^5.0.1
typescript: ^5.1.6
languageName: unknown
linkType: soft
"@medusajs/auth-google@workspace:packages/modules/providers/auth-google": "@medusajs/auth-google@workspace:packages/modules/providers/auth-google":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/auth-google@workspace:packages/modules/providers/auth-google" resolution: "@medusajs/auth-google@workspace:packages/modules/providers/auth-google"