Skip to content
This repository was archived by the owner on Aug 21, 2024. It is now read-only.

Commit c40dcd4

Browse files
committed
Updated JWT authentication to handle RSA signatures
New authentication-setting fields for JWT algorithm and JWT public key. Keys should be inserted as strings with '\n' replacing new lines. Replaced the last of the API.instance.client uses with Engine.instance.api Added a new service that will return the JWT public key. Replaced current jsonwebtoken.decode calls with jsonwebtoken.verify. decode does not check if the signature is valid. Resolves IR-3827
1 parent 65bdc5a commit c40dcd4

File tree

27 files changed

+436
-86
lines changed

27 files changed

+436
-86
lines changed

packages/client-core/i18n/en/admin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@
367367
"service": "Service",
368368
"githubAppId": "App ID (Enter for GitHub App, omit for OAuth App)",
369369
"secret": "Secret",
370+
"jwtAlgorithm": "JWT Algorithm",
371+
"jwtPublicKey": "JWT Public Key",
370372
"entity": "Entity",
371373
"authStrategies": "Authentication Strategies",
372374
"userName": "User Name",

packages/client-core/src/API.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,17 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
2323
Ethereal Engine. All Rights Reserved.
2424
*/
2525

26-
import type { AuthenticationClient } from '@feathersjs/authentication-client'
2726
import authentication from '@feathersjs/authentication-client'
2827
import feathers from '@feathersjs/client'
29-
import type { FeathersApplication } from '@feathersjs/feathers'
3028
import Primus from 'primus-client'
3129

32-
import type { ServiceTypes } from '@etherealengine/common/declarations'
3330
import config from '@etherealengine/common/src/config'
3431
import { Engine } from '@etherealengine/ecs/src/Engine'
3532

3633
import primusClient from './util/primus-client'
3734

38-
export type FeathersClient = FeathersApplication<ServiceTypes> &
39-
AuthenticationClient & {
40-
primus: Primus
41-
authentication: AuthenticationClient
42-
}
43-
4435
/**@deprecated - use 'Engine.instance.api' instead */
4536
export class API {
46-
/**@deprecated - use 'Engine.instance.api' instead */
47-
static instance: API
48-
client: FeathersClient
49-
5037
static createAPI = () => {
5138
const feathersClient = feathers()
5239

@@ -61,13 +48,8 @@ export class API {
6148
})
6249
)
6350

64-
primus.on('reconnected', () => API.instance.client.reAuthenticate(true))
65-
66-
API.instance = new API()
67-
API.instance.client = feathersClient as any
51+
primus.on('reconnected', () => feathersClient.reAuthenticate(true))
6852

6953
Engine.instance.api = feathersClient
7054
}
7155
}
72-
73-
globalThis.API = API

packages/client-core/src/admin/components/settings/tabs/authentication.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,20 @@ const AuthenticationTab = forwardRef(({ open }: { open: boolean }, ref: React.Mu
199199
/>
200200

201201
<Input
202+
className="col-span-1"
203+
label={t('admin:components.setting.entity')}
204+
value={authSetting?.entity || ''}
205+
disabled
206+
/>
207+
208+
<Input
209+
className="col-span-1"
210+
label={t('admin:components.setting.jwtAlgorithm')}
211+
value={authSetting?.jwtAlgorithm || ''}
212+
disabled
213+
/>
214+
215+
<PasswordInput
202216
className="col-span-1"
203217
label={t('admin:components.setting.secret')}
204218
value={authSetting?.secret || ''}
@@ -207,8 +221,8 @@ const AuthenticationTab = forwardRef(({ open }: { open: boolean }, ref: React.Mu
207221

208222
<Input
209223
className="col-span-1"
210-
label={t('admin:components.setting.entity')}
211-
value={authSetting?.entity || ''}
224+
label={t('admin:components.setting.jwtPublicKey')}
225+
value={authSetting?.jwtPublicKey || ''}
212226
disabled
213227
/>
214228
</div>

packages/client-core/src/components/InstanceChat/InstanceChat.skiptest.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ import { createRoot } from 'react-dom/client'
3030

3131
import { ChannelID, MessageID, UserID } from '@etherealengine/common/src/schema.type.module'
3232
import { createEngine } from '@etherealengine/ecs'
33+
import { Engine } from '@etherealengine/ecs/src/Engine'
3334
import { getMutableState } from '@etherealengine/hyperflux'
3435

3536
import { InstanceChat } from '.'
3637
import { createDOM } from '../../../tests/createDOM'
3738
import { createMockAPI } from '../../../tests/createMockAPI'
38-
import { API } from '../../API'
3939
import { ChannelState } from '../../social/services/ChannelService'
4040

4141
describe('Instance Chat Component', () => {
@@ -46,7 +46,7 @@ describe('Instance Chat Component', () => {
4646
rootContainer = document.createElement('div')
4747
document.body.appendChild(rootContainer)
4848
createEngine()
49-
API.instance = createMockAPI()
49+
Engine.instance.api = createMockAPI()
5050
})
5151

5252
afterEach(() => {

packages/client-core/src/social/services/LocationService.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import { Engine } from '@etherealengine/ecs/src/Engine'
3737
import { defineState, getMutableState, getState } from '@etherealengine/hyperflux'
3838

3939
import { useEffect } from 'react'
40-
import { API } from '../../API'
4140
import { NotificationService } from '../../common/services/NotificationService'
4241
import { AuthState } from '../../user/services/AuthService'
4342

@@ -141,15 +140,15 @@ export const LocationService = {
141140
getLocation: async (locationId: LocationID) => {
142141
try {
143142
LocationState.fetchingCurrentSocialLocation()
144-
const location = await API.instance.client.service(locationPath).get(locationId)
143+
const location = await Engine.instance.api.service(locationPath).get(locationId)
145144
LocationState.socialLocationRetrieved(location)
146145
} catch (err) {
147146
NotificationService.dispatchNotify(err.message, { variant: 'error' })
148147
}
149148
},
150149
getLocationByName: async (locationName: string) => {
151150
LocationState.fetchingCurrentSocialLocation()
152-
const locationResult = (await API.instance.client.service(locationPath).find({
151+
const locationResult = (await Engine.instance.api.service(locationPath).find({
153152
query: {
154153
slugifiedName: locationName
155154
}
@@ -167,7 +166,7 @@ export const LocationService = {
167166
}
168167
},
169168
getLobby: async () => {
170-
const lobbyResult = (await API.instance.client.service(locationPath).find({
169+
const lobbyResult = (await Engine.instance.api.service(locationPath).find({
171170
query: {
172171
isLobby: true,
173172
$limit: 1
@@ -182,7 +181,7 @@ export const LocationService = {
182181
},
183182
banUserFromLocation: async (userId: UserID, locationId: LocationID) => {
184183
try {
185-
await API.instance.client.service(locationBanPath).create({
184+
await Engine.instance.api.service(locationBanPath).create({
186185
userId: userId,
187186
locationId: locationId
188187
})

packages/client-core/src/user/components/UserMenu/menus/LocationMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import React, { useEffect, useState } from 'react'
2828
import { useTranslation } from 'react-i18next'
2929

3030
import { locationPath, LocationType } from '@etherealengine/common/src/schema.type.module'
31+
import { Engine } from '@etherealengine/ecs/src/Engine'
3132
import Button from '@etherealengine/ui/src/primitives/mui/Button'
3233
import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
3334
import InputAdornment from '@etherealengine/ui/src/primitives/mui/InputAdornment'
@@ -40,7 +41,6 @@ import TableRow from '@etherealengine/ui/src/primitives/mui/TableRow'
4041
import TextField from '@etherealengine/ui/src/primitives/mui/TextField'
4142
import Typography from '@etherealengine/ui/src/primitives/mui/Typography'
4243

43-
import { API } from '../../../../API'
4444
import { LocationSeed } from '../../../../social/services/LocationService'
4545
import styles from '../index.module.scss'
4646

@@ -68,7 +68,7 @@ const LocationMenu = (props: Props) => {
6868
}, [])
6969

7070
const fetchLocations = (page: number, rows: number, search?: string) => {
71-
API.instance.client
71+
Engine.instance.api
7272
.service(locationPath)
7373
.find({
7474
query: {

packages/client-core/src/user/services/AuthService.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
userPath,
5858
userSettingPath
5959
} from '@etherealengine/common/src/schema.type.module'
60+
import type { FeathersClient } from '@etherealengine/ecs/src/Engine'
6061
import { Engine } from '@etherealengine/ecs/src/Engine'
6162
import {
6263
defineState,
@@ -65,7 +66,6 @@ import {
6566
syncStateWithLocalStorage,
6667
useHookstate
6768
} from '@etherealengine/hyperflux'
68-
import { API } from '../../API'
6969
import { NotificationService } from '../../common/services/NotificationService'
7070

7171
export const logger = multiLogger.child({ component: 'client-core:AuthService' })
@@ -170,7 +170,7 @@ export interface LinkedInLoginForm {
170170
*/
171171
async function _resetToGuestToken(options = { reset: true }) {
172172
if (options.reset) {
173-
await API.instance.client.authentication.reset()
173+
await (Engine.instance.api as FeathersClient).authentication.reset()
174174
}
175175
const newProvider = await Engine.instance.api.service(identityProviderPath).create({
176176
type: 'guest',
@@ -179,7 +179,7 @@ async function _resetToGuestToken(options = { reset: true }) {
179179
})
180180
const accessToken = newProvider.accessToken!
181181
console.log(`Created new guest accessToken: ${accessToken}`)
182-
await API.instance.client.authentication.setAccessToken(accessToken as string)
182+
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
183183
return accessToken
184184
}
185185

@@ -195,22 +195,26 @@ export const AuthService = {
195195
const accessToken = !forceClientAuthReset && authState?.authUser?.accessToken?.value
196196

197197
if (forceClientAuthReset) {
198-
await API.instance.client.authentication.reset()
198+
await (Engine.instance.api as FeathersClient).authentication.reset()
199199
}
200200
if (accessToken) {
201-
await API.instance.client.authentication.setAccessToken(accessToken as string)
201+
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
202202
} else {
203203
await _resetToGuestToken({ reset: false })
204204
}
205205

206206
let res: AuthenticationResult
207207
try {
208-
res = await API.instance.client.reAuthenticate()
208+
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
209209
} catch (err) {
210-
if (err.className === 'not-found' || (err.className === 'not-authenticated' && err.message === 'jwt expired')) {
210+
if (
211+
err.className === 'not-found' ||
212+
(err.className === 'not-authenticated' && err.message === 'jwt expired') ||
213+
(err.className === 'not-authenticated' && err.message === 'invalid algorithm')
214+
) {
211215
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
212216
await _resetToGuestToken()
213-
res = await API.instance.client.reAuthenticate()
217+
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
214218
} else {
215219
logger.error(err, 'Error re-authenticating')
216220
throw err
@@ -222,7 +226,7 @@ export const AuthService = {
222226
if (!identityProvider?.id) {
223227
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
224228
await _resetToGuestToken()
225-
res = await API.instance.client.reAuthenticate()
229+
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
226230
}
227231
const authUser = resolveAuthUser(res)
228232
// authUser is now { accessToken, authentication, identityProvider }
@@ -243,15 +247,14 @@ export const AuthService = {
243247

244248
async loadUserData(userId: UserID) {
245249
try {
246-
const client = API.instance.client
247-
const user = await client.service(userPath).get(userId)
250+
const user = await Engine.instance.api.service(userPath).get(userId)
248251
if (!user.userSetting) {
249-
const settingsRes = (await client
252+
const settingsRes = (await Engine.instance.api
250253
.service(userSettingPath)
251254
.find({ query: { userId: userId } })) as Paginated<UserSettingType>
252255

253256
if (settingsRes.total === 0) {
254-
user.userSetting = await client.service(userSettingPath).create({ userId: userId })
257+
user.userSetting = await Engine.instance.api.service(userSettingPath).create({ userId: userId })
255258
} else {
256259
user.userSetting = settingsRes.data[0]
257260
}
@@ -278,7 +281,7 @@ export const AuthService = {
278281
authState.merge({ isProcessing: true, error: '' })
279282

280283
try {
281-
const authenticationResult = await API.instance.client.authenticate({
284+
const authenticationResult = await (Engine.instance.api as FeathersClient).authenticate({
282285
strategy: 'local',
283286
email: form.email,
284287
password: form.password
@@ -392,8 +395,8 @@ export const AuthService = {
392395

393396
if (newTokenResult?.token) {
394397
getMutableState(AuthState).merge({ isProcessing: true, error: '' })
395-
await API.instance.client.authentication.setAccessToken(newTokenResult.token)
396-
const res = await API.instance.client.reAuthenticate(true)
398+
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(newTokenResult.token)
399+
const res = await (Engine.instance.api as FeathersClient).reAuthenticate(true)
397400
const authUser = resolveAuthUser(res)
398401
await Engine.instance.api.service(identityProviderPath).remove(ipToRemove.id)
399402
const authState = getMutableState(AuthState)
@@ -409,8 +412,8 @@ export const AuthService = {
409412
const authState = getMutableState(AuthState)
410413
authState.merge({ isProcessing: true, error: '' })
411414
try {
412-
await API.instance.client.authentication.setAccessToken(accessToken as string)
413-
const res = await API.instance.client.authenticate({
415+
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
416+
const res = await (Engine.instance.api as FeathersClient).authenticate({
414417
strategy: 'jwt',
415418
accessToken
416419
})
@@ -459,7 +462,7 @@ export const AuthService = {
459462
const authState = getMutableState(AuthState)
460463
authState.merge({ isProcessing: true, error: '' })
461464
try {
462-
await API.instance.client.logout()
465+
await (Engine.instance.api as FeathersClient).logout()
463466
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
464467
} catch (_) {
465468
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })

packages/client-core/src/util/wait-for-client-authenticated.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
2323
Ethereal Engine. All Rights Reserved.
2424
*/
2525

26-
import { Engine } from '@etherealengine/ecs/src/Engine'
27-
28-
import { FeathersClient } from '../API'
26+
import { Engine, FeathersClient } from '@etherealengine/ecs/src/Engine'
2927

3028
async function waitForClientAuthenticated(): Promise<void> {
3129
const api = Engine.instance.api as FeathersClient

packages/client-core/tests/createMockAPI.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
2323
Ethereal Engine. All Rights Reserved.
2424
*/
2525

26-
import { API } from '../src/API'
26+
import type { FeathersClient } from '@etherealengine/ecs/src/Engine'
2727

2828
type MockFeathers = {
2929
on: (type: string, cb: () => void) => void
@@ -42,23 +42,21 @@ type ServicesToMock = {
4242

4343
export const createMockAPI = (servicesToMock?: ServicesToMock) => {
4444
return {
45-
client: {
46-
service: (service: string) => {
47-
if (servicesToMock && servicesToMock[service]) {
48-
return servicesToMock[service]
49-
} else {
50-
return {
51-
on: (type, cb) => {},
52-
off: (type, cb) => {},
53-
find: (type) => {},
54-
get: (type) => {},
55-
create: (type) => {},
56-
patch: (type) => {},
57-
update: (type) => {},
58-
remove: (type) => {}
59-
}
45+
service: (service: string) => {
46+
if (servicesToMock && servicesToMock[service]) {
47+
return servicesToMock[service]
48+
} else {
49+
return {
50+
on: (type, cb) => {},
51+
off: (type, cb) => {},
52+
find: (type) => {},
53+
get: (type) => {},
54+
create: (type) => {},
55+
patch: (type) => {},
56+
update: (type) => {},
57+
remove: (type) => {}
6058
}
6159
}
6260
}
63-
} as unknown as API
61+
} as FeathersClient
6462
}

packages/common/src/schemas/setting/authentication-setting.schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export const authenticationSettingSchema = Type.Object(
124124
}),
125125
service: Type.String(),
126126
entity: Type.String(),
127-
secret: Type.String(),
127+
secret: Type.String({ maxLength: 4095 }),
128+
jwtAlgorithm: Type.Optional(Type.String()),
129+
jwtPublicKey: Type.Optional(Type.String({ maxLength: 1023 })),
128130
authStrategies: Type.Array(Type.Ref(authStrategiesSchema)),
129131
jwtOptions: Type.Optional(Type.Ref(authJwtOptionsSchema)),
130132
bearerToken: Type.Optional(Type.Ref(authBearerTokenSchema)),

0 commit comments

Comments
 (0)