Skip to content

Commit

Permalink
uberf-8195: support openid auth (#6654)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexey Zinoviev <[email protected]>
  • Loading branch information
lexiv0re committed Sep 20, 2024
1 parent 4165163 commit 9a35f01
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 13 deletions.
29 changes: 28 additions & 1 deletion common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions plugins/login-resources/src/components/Providers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,10 @@
{
name: 'github',
component: Github
},
{
name: 'openid',
component: OpenId
}
]
Expand Down
15 changes: 15 additions & 0 deletions plugins/login-resources/src/components/icons/OpenId.svelte
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 10 additions & 0 deletions plugins/login-resources/src/components/providers/OpenId.svelte
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions pods/authProviders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion pods/authProviders/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pods/authProviders/src/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions pods/authProviders/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
119 changes: 119 additions & 0 deletions pods/authProviders/src/openid.ts
Original file line number Diff line number Diff line change
@@ -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'
}
10 changes: 6 additions & 4 deletions pods/authProviders/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
})
})
)

Expand Down
7 changes: 4 additions & 3 deletions server/account-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 9a35f01

Please sign in to comment.