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

Commit

Permalink
Updated JWT authentication to handle RSA signatures
Browse files Browse the repository at this point in the history
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
  • Loading branch information
barankyle committed Aug 14, 2024
1 parent 65bdc5a commit c40dcd4
Show file tree
Hide file tree
Showing 27 changed files with 436 additions and 86 deletions.
2 changes: 2 additions & 0 deletions packages/client-core/i18n/en/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@
"service": "Service",
"githubAppId": "App ID (Enter for GitHub App, omit for OAuth App)",
"secret": "Secret",
"jwtAlgorithm": "JWT Algorithm",
"jwtPublicKey": "JWT Public Key",
"entity": "Entity",
"authStrategies": "Authentication Strategies",
"userName": "User Name",
Expand Down
20 changes: 1 addition & 19 deletions packages/client-core/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,17 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/

import type { AuthenticationClient } from '@feathersjs/authentication-client'
import authentication from '@feathersjs/authentication-client'
import feathers from '@feathersjs/client'
import type { FeathersApplication } from '@feathersjs/feathers'
import Primus from 'primus-client'

import type { ServiceTypes } from '@etherealengine/common/declarations'
import config from '@etherealengine/common/src/config'
import { Engine } from '@etherealengine/ecs/src/Engine'

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

export type FeathersClient = FeathersApplication<ServiceTypes> &
AuthenticationClient & {
primus: Primus
authentication: AuthenticationClient
}

/**@deprecated - use 'Engine.instance.api' instead */
export class API {
/**@deprecated - use 'Engine.instance.api' instead */
static instance: API
client: FeathersClient

static createAPI = () => {
const feathersClient = feathers()

Expand All @@ -61,13 +48,8 @@ export class API {
})
)

primus.on('reconnected', () => API.instance.client.reAuthenticate(true))

API.instance = new API()
API.instance.client = feathersClient as any
primus.on('reconnected', () => feathersClient.reAuthenticate(true))

Engine.instance.api = feathersClient
}
}

globalThis.API = API
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ const AuthenticationTab = forwardRef(({ open }: { open: boolean }, ref: React.Mu
/>

<Input
className="col-span-1"
label={t('admin:components.setting.entity')}
value={authSetting?.entity || ''}
disabled
/>

<Input
className="col-span-1"
label={t('admin:components.setting.jwtAlgorithm')}
value={authSetting?.jwtAlgorithm || ''}
disabled
/>

<PasswordInput
className="col-span-1"
label={t('admin:components.setting.secret')}
value={authSetting?.secret || ''}
Expand All @@ -207,8 +221,8 @@ const AuthenticationTab = forwardRef(({ open }: { open: boolean }, ref: React.Mu

<Input
className="col-span-1"
label={t('admin:components.setting.entity')}
value={authSetting?.entity || ''}
label={t('admin:components.setting.jwtPublicKey')}
value={authSetting?.jwtPublicKey || ''}
disabled
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ import { createRoot } from 'react-dom/client'

import { ChannelID, MessageID, UserID } from '@etherealengine/common/src/schema.type.module'
import { createEngine } from '@etherealengine/ecs'
import { Engine } from '@etherealengine/ecs/src/Engine'
import { getMutableState } from '@etherealengine/hyperflux'

import { InstanceChat } from '.'
import { createDOM } from '../../../tests/createDOM'
import { createMockAPI } from '../../../tests/createMockAPI'
import { API } from '../../API'
import { ChannelState } from '../../social/services/ChannelService'

describe('Instance Chat Component', () => {
Expand All @@ -46,7 +46,7 @@ describe('Instance Chat Component', () => {
rootContainer = document.createElement('div')
document.body.appendChild(rootContainer)
createEngine()
API.instance = createMockAPI()
Engine.instance.api = createMockAPI()
})

afterEach(() => {
Expand Down
9 changes: 4 additions & 5 deletions packages/client-core/src/social/services/LocationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { Engine } from '@etherealengine/ecs/src/Engine'
import { defineState, getMutableState, getState } from '@etherealengine/hyperflux'

import { useEffect } from 'react'
import { API } from '../../API'
import { NotificationService } from '../../common/services/NotificationService'
import { AuthState } from '../../user/services/AuthService'

Expand Down Expand Up @@ -141,15 +140,15 @@ export const LocationService = {
getLocation: async (locationId: LocationID) => {
try {
LocationState.fetchingCurrentSocialLocation()
const location = await API.instance.client.service(locationPath).get(locationId)
const location = await Engine.instance.api.service(locationPath).get(locationId)
LocationState.socialLocationRetrieved(location)
} catch (err) {
NotificationService.dispatchNotify(err.message, { variant: 'error' })
}
},
getLocationByName: async (locationName: string) => {
LocationState.fetchingCurrentSocialLocation()
const locationResult = (await API.instance.client.service(locationPath).find({
const locationResult = (await Engine.instance.api.service(locationPath).find({
query: {
slugifiedName: locationName
}
Expand All @@ -167,7 +166,7 @@ export const LocationService = {
}
},
getLobby: async () => {
const lobbyResult = (await API.instance.client.service(locationPath).find({
const lobbyResult = (await Engine.instance.api.service(locationPath).find({
query: {
isLobby: true,
$limit: 1
Expand All @@ -182,7 +181,7 @@ export const LocationService = {
},
banUserFromLocation: async (userId: UserID, locationId: LocationID) => {
try {
await API.instance.client.service(locationBanPath).create({
await Engine.instance.api.service(locationBanPath).create({
userId: userId,
locationId: locationId
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { locationPath, LocationType } from '@etherealengine/common/src/schema.type.module'
import { Engine } from '@etherealengine/ecs/src/Engine'
import Button from '@etherealengine/ui/src/primitives/mui/Button'
import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import InputAdornment from '@etherealengine/ui/src/primitives/mui/InputAdornment'
Expand All @@ -40,7 +41,6 @@ import TableRow from '@etherealengine/ui/src/primitives/mui/TableRow'
import TextField from '@etherealengine/ui/src/primitives/mui/TextField'
import Typography from '@etherealengine/ui/src/primitives/mui/Typography'

import { API } from '../../../../API'
import { LocationSeed } from '../../../../social/services/LocationService'
import styles from '../index.module.scss'

Expand Down Expand Up @@ -68,7 +68,7 @@ const LocationMenu = (props: Props) => {
}, [])

const fetchLocations = (page: number, rows: number, search?: string) => {
API.instance.client
Engine.instance.api
.service(locationPath)
.find({
query: {
Expand Down
41 changes: 22 additions & 19 deletions packages/client-core/src/user/services/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
userPath,
userSettingPath
} from '@etherealengine/common/src/schema.type.module'
import type { FeathersClient } from '@etherealengine/ecs/src/Engine'
import { Engine } from '@etherealengine/ecs/src/Engine'
import {
defineState,
Expand All @@ -65,7 +66,6 @@ import {
syncStateWithLocalStorage,
useHookstate
} from '@etherealengine/hyperflux'
import { API } from '../../API'
import { NotificationService } from '../../common/services/NotificationService'

export const logger = multiLogger.child({ component: 'client-core:AuthService' })
Expand Down Expand Up @@ -170,7 +170,7 @@ export interface LinkedInLoginForm {
*/
async function _resetToGuestToken(options = { reset: true }) {
if (options.reset) {
await API.instance.client.authentication.reset()
await (Engine.instance.api as FeathersClient).authentication.reset()
}
const newProvider = await Engine.instance.api.service(identityProviderPath).create({
type: 'guest',
Expand All @@ -179,7 +179,7 @@ async function _resetToGuestToken(options = { reset: true }) {
})
const accessToken = newProvider.accessToken!
console.log(`Created new guest accessToken: ${accessToken}`)
await API.instance.client.authentication.setAccessToken(accessToken as string)
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
return accessToken
}

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

if (forceClientAuthReset) {
await API.instance.client.authentication.reset()
await (Engine.instance.api as FeathersClient).authentication.reset()
}
if (accessToken) {
await API.instance.client.authentication.setAccessToken(accessToken as string)
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
} else {
await _resetToGuestToken({ reset: false })
}

let res: AuthenticationResult
try {
res = await API.instance.client.reAuthenticate()
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
} catch (err) {
if (err.className === 'not-found' || (err.className === 'not-authenticated' && err.message === 'jwt expired')) {
if (
err.className === 'not-found' ||
(err.className === 'not-authenticated' && err.message === 'jwt expired') ||
(err.className === 'not-authenticated' && err.message === 'invalid algorithm')
) {
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
await _resetToGuestToken()
res = await API.instance.client.reAuthenticate()
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
} else {
logger.error(err, 'Error re-authenticating')
throw err
Expand All @@ -222,7 +226,7 @@ export const AuthService = {
if (!identityProvider?.id) {
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
await _resetToGuestToken()
res = await API.instance.client.reAuthenticate()
res = await (Engine.instance.api as FeathersClient).reAuthenticate()
}
const authUser = resolveAuthUser(res)
// authUser is now { accessToken, authentication, identityProvider }
Expand All @@ -243,15 +247,14 @@ export const AuthService = {

async loadUserData(userId: UserID) {
try {
const client = API.instance.client
const user = await client.service(userPath).get(userId)
const user = await Engine.instance.api.service(userPath).get(userId)
if (!user.userSetting) {
const settingsRes = (await client
const settingsRes = (await Engine.instance.api
.service(userSettingPath)
.find({ query: { userId: userId } })) as Paginated<UserSettingType>

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

try {
const authenticationResult = await API.instance.client.authenticate({
const authenticationResult = await (Engine.instance.api as FeathersClient).authenticate({
strategy: 'local',
email: form.email,
password: form.password
Expand Down Expand Up @@ -392,8 +395,8 @@ export const AuthService = {

if (newTokenResult?.token) {
getMutableState(AuthState).merge({ isProcessing: true, error: '' })
await API.instance.client.authentication.setAccessToken(newTokenResult.token)
const res = await API.instance.client.reAuthenticate(true)
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(newTokenResult.token)
const res = await (Engine.instance.api as FeathersClient).reAuthenticate(true)
const authUser = resolveAuthUser(res)
await Engine.instance.api.service(identityProviderPath).remove(ipToRemove.id)
const authState = getMutableState(AuthState)
Expand All @@ -409,8 +412,8 @@ export const AuthService = {
const authState = getMutableState(AuthState)
authState.merge({ isProcessing: true, error: '' })
try {
await API.instance.client.authentication.setAccessToken(accessToken as string)
const res = await API.instance.client.authenticate({
await (Engine.instance.api as FeathersClient).authentication.setAccessToken(accessToken as string)
const res = await (Engine.instance.api as FeathersClient).authenticate({
strategy: 'jwt',
accessToken
})
Expand Down Expand Up @@ -459,7 +462,7 @@ export const AuthService = {
const authState = getMutableState(AuthState)
authState.merge({ isProcessing: true, error: '' })
try {
await API.instance.client.logout()
await (Engine.instance.api as FeathersClient).logout()
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
} catch (_) {
authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/

import { Engine } from '@etherealengine/ecs/src/Engine'

import { FeathersClient } from '../API'
import { Engine, FeathersClient } from '@etherealengine/ecs/src/Engine'

async function waitForClientAuthenticated(): Promise<void> {
const api = Engine.instance.api as FeathersClient
Expand Down
32 changes: 15 additions & 17 deletions packages/client-core/tests/createMockAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/

import { API } from '../src/API'
import type { FeathersClient } from '@etherealengine/ecs/src/Engine'

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

export const createMockAPI = (servicesToMock?: ServicesToMock) => {
return {
client: {
service: (service: string) => {
if (servicesToMock && servicesToMock[service]) {
return servicesToMock[service]
} else {
return {
on: (type, cb) => {},
off: (type, cb) => {},
find: (type) => {},
get: (type) => {},
create: (type) => {},
patch: (type) => {},
update: (type) => {},
remove: (type) => {}
}
service: (service: string) => {
if (servicesToMock && servicesToMock[service]) {
return servicesToMock[service]
} else {
return {
on: (type, cb) => {},
off: (type, cb) => {},
find: (type) => {},
get: (type) => {},
create: (type) => {},
patch: (type) => {},
update: (type) => {},
remove: (type) => {}
}
}
}
} as unknown as API
} as FeathersClient
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export const authenticationSettingSchema = Type.Object(
}),
service: Type.String(),
entity: Type.String(),
secret: Type.String(),
secret: Type.String({ maxLength: 4095 }),
jwtAlgorithm: Type.Optional(Type.String()),
jwtPublicKey: Type.Optional(Type.String({ maxLength: 1023 })),
authStrategies: Type.Array(Type.Ref(authStrategiesSchema)),
jwtOptions: Type.Optional(Type.Ref(authJwtOptionsSchema)),
bearerToken: Type.Optional(Type.Ref(authBearerTokenSchema)),
Expand Down
Loading

0 comments on commit c40dcd4

Please sign in to comment.