mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
uberf-8195: support openid auth (#6654)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
4165163e96
commit
9a35f013ad
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
15
plugins/login-resources/src/components/icons/OpenId.svelte
Normal file
15
plugins/login-resources/src/components/icons/OpenId.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<svg viewBox="0 0 512 512" width="1.5rem" height="1.5rem" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect
|
||||
height="512"
|
||||
rx="64"
|
||||
ry="64"
|
||||
style="fill:#f68423;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
width="512"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
<path
|
||||
d="m 416.99957,216.95084 c -39.2625,-22.18682 -78.9827,-34.64025 -117.0052,-39.52028 V 73.776502 l -60.66712,39.049158 v 63.02711 c -188.010218,14.68899 -295.068752,208.65924 0,262.37073 l 60.66712,-39.04916 V 218.28862 c 24.3468,4.22226 50.4759,12.17342 78.0094,24.69351 l -34.6758,21.70238 h 112.6719 v -73.76497 l -39.0003,26.0313 z m -177.67682,-1.66668 v 183.89018 c -182.841238,-23.72461 -140.809838,-171.94788 0,-183.89018 z"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 751 B |
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import OpenId from '../icons/OpenId.svelte'
|
||||
import login from '../../plugin'
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<OpenId />
|
||||
<Label label={login.string.ContinueWith} params={{ provider: 'OpenId' }} />
|
||||
</div>
|
@ -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",
|
||||
|
@ -12,7 +12,7 @@ export function registerGithub (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
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
|
||||
|
@ -12,7 +12,7 @@ export function registerGoogle (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
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 {
|
||||
|
@ -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<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
db: Promise<Db>,
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
) => string | undefined
|
||||
@ -24,7 +25,7 @@ export function registerProviders (
|
||||
ctx: MeasureContext,
|
||||
app: Koa<Koa.DefaultState, Koa.DefaultContext>,
|
||||
router: Router<any, any>,
|
||||
db: Db,
|
||||
db: Promise<Db>,
|
||||
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)
|
||||
|
119
pods/authProviders/src/openid.ts
Normal file
119
pods/authProviders/src/openid.ts
Normal file
@ -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<any, any>,
|
||||
accountsUrl: string,
|
||||
dbPromise: Promise<Db>,
|
||||
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'
|
||||
}
|
@ -12,7 +12,7 @@ export function registerToken (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
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))
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user