Merge pull request #906 from gradido/pubKey-in-JWT

refactor: Provide pubKey in Resolver Context to Avoid API Calls
This commit is contained in:
Moriz Wahl 2021-10-01 12:52:42 +02:00 committed by GitHub
commit e7f68d0da6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 51 additions and 35 deletions

View File

@ -15,9 +15,10 @@ export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
)
context.sessionId = decoded.sessionId
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId) })
context.pubKey = decoded.pubKey
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId, decoded.pubKey) })
return result.success
}
}
return false
throw new Error('401 Unauthorized')
}

View File

@ -2,9 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import CONFIG from '../../config'
import { Balance } from '../models/Balance'
import { apiGet } from '../../apis/HttpRequest'
import { User as dbUser } from '../../typeorm/entity/User'
import { Balance as dbBalance } from '../../typeorm/entity/Balance'
import { calculateDecay } from '../../util/decay'
@ -15,12 +13,8 @@ export class BalanceResolver {
@Authorized()
@Query(() => Balance)
async balance(@Ctx() context: any): Promise<Balance> {
// get public key for current logged in user
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
if (!result.success) throw new Error(result.data)
// load user and balance
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
const userEntity = await dbUser.findByPubkeyHex(context.pubKey)
const balanceEntity = await dbBalance.findByUser(userEntity.id)
let balance: Balance
const now = new Date()
@ -39,7 +33,6 @@ export class BalanceResolver {
decay_date: now.toString(),
})
}
return balance
}
}

View File

@ -18,18 +18,14 @@ export class GdtResolver {
{ currentPage = 1, pageSize = 5, order = 'DESC' }: GdtTransactionSessionIdInput,
@Ctx() context: any,
): Promise<GdtEntryList> {
// get public key for current logged in user
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
if (!result.success) throw new Error(result.data)
// load user
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
const userEntity = await dbUser.findByPubkeyHex(context.pubKey)
const resultGDT = await apiGet(
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
)
if (!resultGDT.success) {
throw new Error(result.data)
throw new Error(resultGDT.data)
}
return new GdtEntryList(resultGDT.data)

View File

@ -22,6 +22,7 @@ import {
klicktippNewsletterStateMiddleware,
} from '../../middleware/klicktippMiddleware'
import { CheckEmailResponse } from '../models/CheckEmailResponse'
@Resolver()
export class UserResolver {
@Query(() => User)
@ -35,7 +36,10 @@ export class UserResolver {
throw new Error(result.data)
}
context.setHeaders.push({ key: 'token', value: encode(result.data.session_id) })
context.setHeaders.push({
key: 'token',
value: encode(result.data.session_id, result.data.user.public_hex),
})
return new User(result.data.user)
}

View File

@ -1,18 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import jwt from 'jsonwebtoken'
import jwt, { JwtPayload } from 'jsonwebtoken'
import CONFIG from '../config/'
export default (token: string): any => {
if (!token) return new Error('401 Unauthorized')
interface CustomJwtPayload extends JwtPayload {
sessionId: number
pubKey: Buffer
}
type DecodedJwt = {
token: string
sessionId: number
pubKey: Buffer
}
export default (token: string): DecodedJwt => {
if (!token) throw new Error('401 Unauthorized')
let sessionId = null
let pubKey = null
try {
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sub
const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sessionId
pubKey = decoded.pubKey
return {
token,
sessionId,
pubKey,
}
} catch (err) {
throw new Error('403.13 - Client certificate revoked')

View File

@ -5,8 +5,8 @@ import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
// Generate an Access Token
export default function encode(sessionId: string): string {
const token = jwt.sign({ sessionId }, CONFIG.JWT_SECRET, {
export default function encode(sessionId: number, pubKey: Buffer): string {
const token = jwt.sign({ sessionId, pubKey }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
subject: sessionId.toString(),
})

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const context = (args: any) => {
const authorization = args.req.headers.authorization
let token = null

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const plugins = [
{
requestDidStart() {

View File

@ -1,6 +1,8 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
// import { Group } from "./Group"
// Moriz: I do not like the idea of having two user tables
@Entity('state_users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
@ -27,6 +29,9 @@ export class User extends BaseEntity {
@Column()
disabled: boolean
// Moriz: I am voting for the data mapper implementation.
// see: https://typeorm.io/#/active-record-data-mapper/what-is-the-data-mapper-pattern
// We should discuss this ASAP
static findByPubkeyHex(pubkeyHex: string): Promise<User> {
return this.createQueryBuilder('user')
.where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex })

View File

@ -57,7 +57,7 @@ describe('UserCard_Language', () => {
})
it('has change language as text', () => {
expect(wrapper.find('a').text()).toBe('form.changeLanguage')
expect(wrapper.find('a').text()).toBe('settings.language.changeLanguage')
})
it('has no select field by default', () => {
@ -87,23 +87,23 @@ describe('UserCard_Language', () => {
describe('change language', () => {
it('does not enable the submit button when same language is chosen', () => {
wrapper.findAll('option').at(1).setSelected()
wrapper.findAll('option').at(0).setSelected()
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
it('enables the submit button when other language is chosen', async () => {
await wrapper.findAll('option').at(2).setSelected()
await wrapper.findAll('option').at(1).setSelected()
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe(undefined)
})
it('updates language data in component', async () => {
await wrapper.findAll('option').at(2).setSelected()
await wrapper.findAll('option').at(1).setSelected()
expect(wrapper.vm.language).toBe('en')
})
describe('cancel edit', () => {
beforeEach(async () => {
await wrapper.findAll('option').at(2).setSelected()
await wrapper.findAll('option').at(1).setSelected()
wrapper.find('a').trigger('click')
})
@ -118,7 +118,7 @@ describe('UserCard_Language', () => {
describe('submit', () => {
beforeEach(async () => {
await wrapper.findAll('option').at(2).setSelected()
await wrapper.findAll('option').at(1).setSelected()
wrapper.find('form').trigger('submit')
})
@ -147,7 +147,7 @@ describe('UserCard_Language', () => {
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('languages.success')
expect(toastSuccessMock).toBeCalledWith('settings.language.success')
})
})

View File

@ -104,7 +104,7 @@ describe('UserCard_Newsletter', () => {
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('setting.newsletterFalse')
expect(toastSuccessMock).toBeCalledWith('settings.newsletter.newsletterFalse')
})
})