Merge pull request #3306 from gradido/3305-feature-gms-user-search-backend-authentication-handshake

feat(backend): gms user-search - backend authentication-handshake
This commit is contained in:
clauspeterhuebner 2024-04-03 22:51:31 +02:00 committed by GitHub
commit b4986d7b3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 137 additions and 2 deletions

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 84,
lines: 83,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -183,3 +183,40 @@ export async function updateGmsUser(apiKey: string, user: GmsUser): Promise<bool
return false
}
}
export async function verifyAuthToken(
// apiKey: string,
communityUuid: string,
token: string,
): Promise<string> {
const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/')
const service = 'verify-auth-token?token='.concat(token).concat('&uuid=').concat(communityUuid)
const config = {
headers: {
accept: 'application/json',
language: 'en',
timezone: 'UTC',
connection: 'keep-alive',
// authorization: apiKey,
},
}
try {
const result = await axios.get(baseUrl.concat(service), config)
logger.debug('GET-Response of verify-auth-token:', result)
if (result.status !== 200) {
throw new LogError(
'HTTP Status Error in verify-auth-token:',
result.status,
result.statusText,
)
}
logger.debug('responseData:', result.data.responseData)
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const token: string = result.data.responseData.token
logger.debug('verifyAuthToken=', token)
return token
} catch (error: any) {
logger.error('Error in verifyAuthToken:', error)
throw new LogError(error.message)
}
}

View File

@ -37,6 +37,7 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND',
// Moderator
SEARCH_USERS = 'SEARCH_USERS',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',

View File

@ -29,4 +29,5 @@ export const USER_RIGHTS = [
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
RIGHTS.GMS_USER_PLAYGROUND,
]

View File

@ -19,7 +19,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v21.2024-01-06',
EXPECTED: 'v22.2024-03-14',
CURRENT: '',
},
}
@ -146,6 +146,8 @@ const gms = {
GMS_CREATE_USER_THROW_ERRORS: process.env.GMS_CREATE_USER_THROW_ERRORS === 'true' || false,
// koordinates of Illuminz-instance of GMS
GMS_URL: process.env.GMS_HOST ?? 'http://localhost:4044/',
// used as secret postfix attached at the gms community-auth-url endpoint ('/hook/gms/' + 'secret')
GMS_WEBHOOK_SECRET: process.env.GMS_WEBHOOK_SECRET ?? 'secret',
}
export const CONFIG = {

View File

@ -0,0 +1,10 @@
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
export class GmsUserAuthenticationResult {
@Field(() => String)
url: string
@Field(() => String)
token: string
}

View File

@ -25,6 +25,7 @@ import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
import { UserContactType } from '@enum/UserContactType'
import { SearchAdminUsersResult } from '@model/AdminUser'
// import { Location } from '@model/Location'
import { GmsUserAuthenticationResult } from '@model/GmsUserAuthenticationResult'
import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
@ -68,6 +69,7 @@ import random from 'random-bigint'
import { randombytes_random } from 'sodium-native'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlayground'
import { getHomeCommunity } from './util/communities'
import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings'
import { getUserCreations } from './util/creations'
@ -674,6 +676,21 @@ export class UserResolver {
return elopageBuys
}
@Authorized([RIGHTS.GMS_USER_PLAYGROUND])
@Query(() => GmsUserAuthenticationResult)
async authenticateGmsUserSearch(@Ctx() context: Context): Promise<GmsUserAuthenticationResult> {
logger.info(`authUserForGmsUserSearch()...`)
const dbUser = getUser(context)
let result: GmsUserAuthenticationResult
if (context.token) {
result = await authenticateGmsUserPlayground(context.token, dbUser)
logger.info('authUserForGmsUserSearch=', result)
} else {
throw new LogError('authUserForGmsUserSearch without token')
}
return result
}
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
@Query(() => SearchAdminUsersResult)
async searchAdminUsers(

View File

@ -0,0 +1,17 @@
import { User as DbUser } from '@entity/User'
import { verifyAuthToken } from '@/apis/gms/GmsClient'
import { CONFIG } from '@/config'
import { GmsUserAuthenticationResult } from '@/graphql/model/GmsUserAuthenticationResult'
import { backendLogger as logger } from '@/server/logger'
export async function authenticateGmsUserPlayground(
token: string,
dbUser: DbUser,
): Promise<GmsUserAuthenticationResult> {
const result = new GmsUserAuthenticationResult()
result.url = CONFIG.GMS_URL.concat('/playground')
result.token = await verifyAuthToken(dbUser.communityUuid, token)
logger.info('GmsUserAuthenticationResult:', result)
return result
}

View File

@ -15,6 +15,14 @@ export const verifyLogin = gql`
}
}
`
export const authenticateGmsUserSearch = gql`
query {
authenticateGmsUserSearch {
url
token
}
}
`
export const queryOptIn = gql`
query ($optIn: String!) {

View File

@ -13,6 +13,7 @@ import { schema } from '@/graphql/schema'
import { Connection } from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
import { elopageWebhook } from '@/webhook/elopage'
import { gmsWebhook } from '@/webhook/gms'
import { context as serverContext } from './context'
import { cors } from './cors'
@ -94,6 +95,10 @@ export const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
// GMS Webhook
// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.get('/hook/gms/' + CONFIG.GMS_WEBHOOK_SECRET, gmsWebhook)
// Apollo Server
const apollo = new ApolloServer({
schema: await schema(),

View File

@ -37,6 +37,7 @@ const logPlugin = {
const { logger } = requestContext
const { query, mutation, variables, operationName } = requestContext.request
if (operationName !== 'IntrospectionQuery') {
logger.debug('requestDidStart:', requestContext)
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
}

View File

@ -0,0 +1,36 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { User as DbUser } from '@entity/User'
import { decode } from '@/auth/JWT'
export const gmsWebhook = async (req: any, res: any): Promise<void> => {
console.log('GMS Hook received', req.query)
const { token } = req.query
if (!token) {
console.log('gmsWebhook: missing token')
res.status(400).json({ message: 'false' })
return
}
const payload = await decode(token)
console.log('gmsWebhook: decoded token=', payload)
if (!payload) {
console.log('gmsWebhook: invalid token')
res.status(400).json({ message: 'false' })
return
}
const user = await DbUser.findOne({ where: { gradidoID: payload.gradidoID } })
if (!user) {
console.log('gmsWebhook: missing user')
res.status(400).json({ message: 'false' })
return
}
console.log('gmsWebhook: authenticate user=', user.gradidoID, user.firstName, user.lastName)
console.log('gmsWebhook: authentication successful')
res.status(200).json({ userUuid: user.gradidoID })
}