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

Updated JWT authentication to handle RSA signatures #10961

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading