This guide walks through replacing local (email/password) and OAuth authentication in a FeathersJS application with Talon Auth — a passwordless authentication system based on login codes. The examples are based on the feathers-chat application.
@feathersjs/authentication already configureddid:key:... string).talon-auth packagenpm install talon-auth
In your config/default.json, replace the local strategy (and any OAuth strategies) with talon in the authStrategies array, and remove the local and oauth configuration blocks:
{
"authentication": {
"secret": "your-secret",
"authStrategies": [
"jwt",
"talon"
],
"jwtOptions": {
"header": {
"typ": "access"
},
"audience": "https://yourdomain.com",
"algorithm": "HS256",
"expiresIn": "1d"
}
}
}
The local, oauth, and any provider-specific blocks (e.g. github) can be removed entirely.
Replace the contents of src/authentication.ts with a custom TalonStrategy that uses the talon-auth verifier to validate tokens and find-or-create users:
import { AuthenticationBaseStrategy, AuthenticationService, JWTStrategy } from '@feathersjs/authentication'
import { createVerifier } from 'talon-auth'
import type { Application } from './declarations'
declare module './declarations' {
interface ServiceTypes {
authentication: AuthenticationService
}
}
const appId = 'your-talon-app-id'
class TalonStrategy extends AuthenticationBaseStrategy {
verifier = createVerifier({ appId })
async authenticate(authentication: any, params: any) {
const { accessToken } = authentication
const { user: talonUser } = await this.verifier.verify(accessToken)
const usersService = this.app!.service('users')
if (!talonUser) {
throw new Error('Talon user not found')
}
const { email } = talonUser
// Find or create the user by email
const users = await usersService.find({
query: { email },
paginate: false
})
const user = users?.length > 0 ? users[0] : await usersService.create({ email })
return {
authentication: { strategy: 'talon' },
user
}
}
}
export const authentication = (app: Application) => {
const authentication = new AuthenticationService(app)
authentication.register('jwt', new JWTStrategy())
authentication.register('talon', new TalonStrategy())
app.use('authentication', authentication)
}
Since Talon handles authentication, you can remove password-related fields and password hashing from your user schema. In src/services/users/users.schema.ts:
password and any provider ID fields (e.g. githubId) from userSchemapasswordHash import and resolver from @feathersjs/authentication-localuserExternalResolver, userDataSchema, and userPatchResolverBefore:
import { passwordHash } from '@feathersjs/authentication-local'
export const userSchema = Type.Object({
id: Type.Number(),
email: Type.String(),
password: Type.Optional(Type.String()),
githubId: Type.Optional(Type.Number()),
avatar: Type.Optional(Type.String())
}, { $id: 'User', additionalProperties: false })
export const userDataSchema = Type.Pick(userSchema, ['email', 'password', 'githubId', 'avatar'], {
$id: 'UserData',
additionalProperties: false
})
export const userDataResolver = resolve<User, HookContext>({
password: passwordHash({ strategy: 'local' }),
avatar: async (value, user) => { /* ... */ }
})
After:
export const userSchema = Type.Object({
id: Type.Number(),
email: Type.String(),
avatar: Type.Optional(Type.String())
}, { $id: 'User', additionalProperties: false })
export const userDataSchema = Type.Pick(userSchema, ['email', 'avatar'], {
$id: 'UserData',
additionalProperties: false
})
export const userDataResolver = resolve<User, HookContext>({
avatar: async (value, user) => { /* ... */ }
})
Similarly, update userQueryProperties to remove any provider ID fields:
export const userQueryProperties = Type.Pick(userSchema, ['id', 'email'])
<talon-login> web component to the frontendThe <talon-login> element is a drop-in Lit web component that handles the entire login flow (email input, code verification) and hides itself once authenticated.
Add it to your index.html:
<talon-login
id="talon-login"
app-id="your-talon-app-id"
></talon-login>
<script type="module" src="https://esm.sh/talon-auth/login"></script>
Place it before your application scripts so it is available when they run.
Replace the login/signup form logic with Talon's token-based flow. The key change is using talonLogin.getAccessToken() to obtain a token, then passing it to Feathers:
const socket = io()
const client = feathers.socketio(socket)
client.configure(feathers.authentication())
// Reference the <talon-login> element
const talonLogin = document.getElementById('talon-login')
const initialize = async () => {
// getAccessToken() shows the login UI if needed,
// and resolves with a JWT once the user is authenticated
const accessToken = await talonLogin.getAccessToken()
await client.authenticate({
strategy: 'talon',
accessToken
})
// User is now authenticated — show the app
showChat()
}
initialize()
For logout, call both the Feathers client logout and the Talon element's logout:
addEventListener('#logout', 'click', async () => {
await client.logout()
await talonLogin.logout()
})
You can remove all login/signup form templates, the getCredentials() helper, and the login() function — Talon's web component handles the entire authentication UI.
You can remove packages that are no longer needed:
npm uninstall @feathersjs/authentication-local @feathersjs/authentication-oauth
| What changed | Before | After |
|---|---|---|
| Auth strategies | jwt, local (+ optional OAuth) | jwt, talon |
| User fields | email, password, githubId | email |
| Login UI | Custom form in the app | <talon-login> web component |
| Dependencies | @feathersjs/authentication-local, @feathersjs/authentication-oauth | talon-auth |
| Backend verification | Password hash comparison | Offline JWT verification via createVerifier |