diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index 7e335295fa..9a8645a5bf 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -1691,6 +1691,9 @@ dependencies:
openai:
specifier: ^4.56.0
version: 4.56.0(zod@3.23.8)
+ openid-client:
+ specifier: ~5.7.0
+ version: 5.7.0
otp-generator:
specifier: ^4.0.1
version: 4.0.1
@@ -17193,6 +17196,10 @@ packages:
engines: {node: '>= 0.6.0'}
dev: false
+ /jose@4.15.9:
+ resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
+ dev: false
+
/jose@5.6.3:
resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
dev: false
@@ -19076,6 +19083,11 @@ packages:
resolution: {integrity: sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==}
dev: false
+ /object-hash@2.2.0:
+ resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
+ engines: {node: '>= 6'}
+ dev: false
+
/object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@@ -19210,6 +19222,11 @@ packages:
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
dev: false
+ /oidc-token-hash@5.0.3:
+ resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
+ engines: {node: ^10.13.0 || >=12.0.0}
+ dev: false
+
/omggif@1.0.10:
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
dev: false
@@ -19298,6 +19315,15 @@ packages:
hasBin: true
dev: false
+ /openid-client@5.7.0:
+ resolution: {integrity: sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==}
+ dependencies:
+ jose: 4.15.9
+ lru-cache: 6.0.0
+ object-hash: 2.2.0
+ oidc-token-hash: 5.0.3
+ dev: false
+
/option@0.2.4:
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
dev: false
@@ -25220,7 +25246,7 @@ packages:
dev: false
file:projects/auth-providers.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
- resolution: {integrity: sha512-RDD+zkNosRxJuY+bq2skRGbRPr4saOaAvlkKv3gvvIXT5RxnmOXnznS6haxIcXw4VA96oxv08U9Mu8+3PjgpwQ==, tarball: file:projects/auth-providers.tgz}
+ resolution: {integrity: sha512-0YbyLxnpaSZfdCfdlt5T9LI/TmahO7fbLohsHvXQVFb2J1InAIyu1WWzbS92CLYLHxQtJmSIOAbPVbRV1wPWbw==, tarball: file:projects/auth-providers.tgz}
id: file:projects/auth-providers.tgz
name: '@rush-temp/auth-providers'
version: 0.0.0
@@ -25248,6 +25274,7 @@ packages:
koa-router: 12.0.1
koa-session: 6.4.0
mongodb: 6.9.0
+ openid-client: 5.7.0
passport-custom: 1.1.1
passport-github2: 0.1.12
passport-google-oauth20: 2.0.0
diff --git a/plugins/login-resources/src/components/Providers.svelte b/plugins/login-resources/src/components/Providers.svelte
index 4e60845186..4753592a75 100644
--- a/plugins/login-resources/src/components/Providers.svelte
+++ b/plugins/login-resources/src/components/Providers.svelte
@@ -7,6 +7,7 @@
import { getProviders } from '../utils'
import Github from './providers/Github.svelte'
import Google from './providers/Google.svelte'
+ import OpenId from './providers/OpenId.svelte'
interface Provider {
name: string
@@ -21,6 +22,10 @@
{
name: 'github',
component: Github
+ },
+ {
+ name: 'openid',
+ component: OpenId
}
]
diff --git a/plugins/login-resources/src/components/icons/OpenId.svelte b/plugins/login-resources/src/components/icons/OpenId.svelte
new file mode 100644
index 0000000000..c977eaecc5
--- /dev/null
+++ b/plugins/login-resources/src/components/icons/OpenId.svelte
@@ -0,0 +1,15 @@
+
diff --git a/plugins/login-resources/src/components/providers/OpenId.svelte b/plugins/login-resources/src/components/providers/OpenId.svelte
new file mode 100644
index 0000000000..1948f535c8
--- /dev/null
+++ b/plugins/login-resources/src/components/providers/OpenId.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/pods/authProviders/package.json b/pods/authProviders/package.json
index 4735797719..20e8c75f2b 100644
--- a/pods/authProviders/package.json
+++ b/pods/authProviders/package.json
@@ -52,6 +52,7 @@
"passport-custom": "~1.1.1",
"passport-google-oauth20": "~2.0.0",
"passport-github2": "~0.1.12",
+ "openid-client": "~5.7.0",
"koa-passport": "^6.0.0",
"koa": "^2.15.3",
"koa-router": "^12.0.1",
diff --git a/pods/authProviders/src/github.ts b/pods/authProviders/src/github.ts
index 57ffae31db..6fc525cd5f 100644
--- a/pods/authProviders/src/github.ts
+++ b/pods/authProviders/src/github.ts
@@ -12,7 +12,7 @@ export function registerGithub (
passport: Passport,
router: Router,
accountsUrl: string,
- db: Db,
+ dbPromise: Promise,
frontUrl: string,
brandings: BrandingMap
): string | undefined {
@@ -69,6 +69,7 @@ export function registerGithub (
let loginInfo: LoginInfo
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
+ const db = await dbPromise
if (state.inviteId != null && state.inviteId !== '') {
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
githubId: ctx.state.user.id
diff --git a/pods/authProviders/src/google.ts b/pods/authProviders/src/google.ts
index 6453165d58..be68fe269a 100644
--- a/pods/authProviders/src/google.ts
+++ b/pods/authProviders/src/google.ts
@@ -12,7 +12,7 @@ export function registerGoogle (
passport: Passport,
router: Router,
accountsUrl: string,
- db: Db,
+ dbPromise: Promise,
frontUrl: string,
brandings: BrandingMap
): string | undefined {
@@ -74,6 +74,7 @@ export function registerGoogle (
let loginInfo: LoginInfo
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
+ const db = await dbPromise
if (state.inviteId != null && state.inviteId !== '') {
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any)
} else {
diff --git a/pods/authProviders/src/index.ts b/pods/authProviders/src/index.ts
index 0ab49993c0..a812761f03 100644
--- a/pods/authProviders/src/index.ts
+++ b/pods/authProviders/src/index.ts
@@ -5,6 +5,7 @@ import session from 'koa-session'
import { Db } from 'mongodb'
import { registerGithub } from './github'
import { registerGoogle } from './google'
+import { registerOpenid } from './openid'
import { registerToken } from './token'
import { BrandingMap, MeasureContext } from '@hcengineering/core'
@@ -15,7 +16,7 @@ export type AuthProvider = (
passport: Passport,
router: Router,
accountsUrl: string,
- db: Db,
+ db: Promise,
frontUrl: string,
brandings: BrandingMap
) => string | undefined
@@ -24,7 +25,7 @@ export function registerProviders (
ctx: MeasureContext,
app: Koa,
router: Router,
- db: Db,
+ db: Promise,
serverSecret: string,
frontUrl: string | undefined,
brandings: BrandingMap
@@ -60,7 +61,7 @@ export function registerProviders (
registerToken(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
const res: string[] = []
- const providers: AuthProvider[] = [registerGoogle, registerGithub]
+ const providers: AuthProvider[] = [registerGoogle, registerGithub, registerOpenid]
for (const provider of providers) {
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
if (value !== undefined) res.push(value)
diff --git a/pods/authProviders/src/openid.ts b/pods/authProviders/src/openid.ts
new file mode 100644
index 0000000000..b517c28664
--- /dev/null
+++ b/pods/authProviders/src/openid.ts
@@ -0,0 +1,119 @@
+//
+// Copyright © 2024 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the f.
+//
+import { joinWithProvider, loginWithProvider, type LoginInfo } from '@hcengineering/account'
+import { BrandingMap, concatLink, MeasureContext, getBranding } from '@hcengineering/core'
+import Router from 'koa-router'
+import { Db } from 'mongodb'
+import { Issuer, Strategy } from 'openid-client'
+import qs from 'querystringify'
+
+import { Passport } from '.'
+import { getHost, safeParseAuthState } from './utils'
+
+export function registerOpenid (
+ measureCtx: MeasureContext,
+ passport: Passport,
+ router: Router,
+ accountsUrl: string,
+ dbPromise: Promise,
+ frontUrl: string,
+ brandings: BrandingMap
+): string | undefined {
+ const openidClientId = process.env.OPENID_CLIENT_ID
+ const openidClientSecret = process.env.OPENID_CLIENT_SECRET
+ const issuer = process.env.OPENID_ISSUER
+
+ const redirectURL = '/auth/openid/callback'
+ if (openidClientId === undefined || openidClientSecret === undefined || issuer === undefined) return
+
+ void Issuer.discover(issuer).then((issuerObj) => {
+ const client = new issuerObj.Client({
+ client_id: openidClientId,
+ client_secret: openidClientSecret,
+ redirect_uris: [concatLink(accountsUrl, redirectURL)],
+ response_types: ['code']
+ })
+
+ passport.use(
+ 'oidc',
+ new Strategy({ client, passReqToCallback: true }, (req: any, tokenSet: any, userinfo: any, done: any) => {
+ return done(null, userinfo)
+ })
+ )
+ })
+
+ router.get('/auth/openid', async (ctx, next) => {
+ measureCtx.info('try auth via', { provider: 'openid' })
+ const host = getHost(ctx.request.headers)
+ const brandingKey = host !== undefined ? brandings[host]?.key ?? undefined : undefined
+ const state = encodeURIComponent(
+ JSON.stringify({
+ inviteId: ctx.query?.inviteId,
+ branding: brandingKey
+ })
+ )
+
+ await passport.authenticate('oidc', {
+ scope: 'openid profile email',
+ state
+ })(ctx, next)
+ })
+
+ router.get(
+ redirectURL,
+ async (ctx, next) => {
+ const state = safeParseAuthState(ctx.query?.state)
+ const branding = getBranding(brandings, state?.branding)
+
+ await passport.authenticate('oidc', {
+ failureRedirect: concatLink(branding?.front ?? frontUrl, '/login')
+ })(ctx, next)
+ },
+ async (ctx, next) => {
+ try {
+ const email = ctx.state.user.email ?? `openid:${ctx.state.user.sub}`
+ const [first, last] = ctx.state.user.name?.split(' ') ?? [ctx.state.user.username, '']
+ measureCtx.info('Provider auth handler', { email, type: 'openid' })
+ if (email !== undefined) {
+ let loginInfo: LoginInfo
+ const state = safeParseAuthState(ctx.query?.state)
+ const branding = getBranding(brandings, state?.branding)
+ const db = await dbPromise
+ if (state.inviteId != null && state.inviteId !== '') {
+ loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
+ openId: ctx.state.user.sub
+ })
+ } else {
+ loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last, {
+ openId: ctx.state.user.sub
+ })
+ }
+
+ const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
+ const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
+
+ measureCtx.info('Success auth, redirect', { email, type: 'openid', target: origin })
+ // Successful authentication, redirect to your application
+ ctx.redirect(`${origin}?${query}`)
+ }
+ } catch (err: any) {
+ measureCtx.error('failed to auth', { err, type: 'openid', user: ctx.state?.user })
+ }
+ await next()
+ }
+ )
+
+ return 'openid'
+}
diff --git a/pods/authProviders/src/token.ts b/pods/authProviders/src/token.ts
index fa2ad0793f..8edbd7d32b 100644
--- a/pods/authProviders/src/token.ts
+++ b/pods/authProviders/src/token.ts
@@ -12,7 +12,7 @@ export function registerToken (
passport: Passport,
router: Router,
accountsUrl: string,
- db: Db,
+ dbPromise: Promise,
frontUrl: string,
brandings: BrandingMap
): string | undefined {
@@ -21,9 +21,11 @@ export function registerToken (
new CustomStrategy(function (req: any, done: any) {
const token = req.body.token ?? req.query.token
- getAccountInfoByToken(measureCtx, db, null, token)
- .then((user: any) => done(null, user))
- .catch((err: any) => done(err))
+ void dbPromise.then((db) => {
+ getAccountInfoByToken(measureCtx, db, null, token)
+ .then((user: any) => done(null, user))
+ .catch((err: any) => done(err))
+ })
})
)
diff --git a/server/account-service/src/index.ts b/server/account-service/src/index.ts
index c57ccaf2a6..03257a6f06 100644
--- a/server/account-service/src/index.ts
+++ b/server/account-service/src/index.ts
@@ -103,10 +103,11 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
)
app.use(bodyParser())
- void client.getClient().then(async (p: MongoClient) => {
- const db = p.db(ACCOUNT_DB)
- registerProviders(measureCtx, app, router, db, serverSecret, frontURL, brandings)
+ const mongoClientPromise = client.getClient()
+ const dbPromise = mongoClientPromise.then((c) => c.db(ACCOUNT_DB))
+ registerProviders(measureCtx, app, router, dbPromise, serverSecret, frontURL, brandings)
+ void dbPromise.then((db) => {
setInterval(
() => {
void cleanExpiredOtp(db)