Skip to content

Commit 72ae9ca

Browse files
committed
feat: better auth plugin structure
1 parent b13c7ad commit 72ae9ca

File tree

16 files changed

+338
-316
lines changed

16 files changed

+338
-316
lines changed

apps/api/package.json

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,38 @@
4040
"license": "ISC",
4141
"dependencies": {
4242
"@codeimage/prisma-models": "workspace:*",
43-
"@fastify/auth": "4.4.0",
44-
"@fastify/autoload": "^5.7.1",
45-
"@fastify/cors": "^8.3.0",
46-
"@fastify/env": "^4.2.0",
47-
"@fastify/jwt": "^6.7.1",
48-
"@fastify/sensible": "^5.2.0",
49-
"@fastify/swagger": "^8.5.1",
50-
"@fastify/swagger-ui": "^1.8.1",
51-
"@fastify/type-provider-typebox": "^3.2.0",
43+
"@fastify/auth": "^4.4.0",
44+
"@fastify/autoload": "^5.8.0",
45+
"@fastify/cors": "^8.4.2",
46+
"@fastify/env": "^4.3.0",
47+
"@fastify/jwt": "^7.2.4",
48+
"@fastify/sensible": "^5.5.0",
49+
"@fastify/swagger": "^8.12.1",
50+
"@fastify/swagger-ui": "^2.0.1",
51+
"@fastify/type-provider-typebox": "^3.5.0",
5252
"@prisma/client": "^4.15.0",
53-
"@sinclair/typebox": "^0.28.15",
54-
"@teamhanko/passkeys-sdk": "0.1.8",
53+
"@sinclair/typebox": "^0.31.28",
54+
"@teamhanko/passkeys-sdk": "^0.1.8",
5555
"auth0": "4.2.0",
5656
"close-with-grace": "^1.2.0",
5757
"dotenv": "^16.1.4",
5858
"dotenv-cli": "^6.0.0",
59-
"fastify": "^4.18.0",
60-
"fastify-auth0-verify": "^1.2.0",
61-
"fastify-cli": "^5.7.1",
59+
"fastify": "^4.25.1",
60+
"fastify-auth0-verify": "^1.2.1",
61+
"fastify-cli": "^5.9.0",
6262
"fastify-healthcheck": "^4.4.0",
63-
"fastify-jwt-jwks": "1.1.4",
64-
"fastify-plugin": "^4.5.0",
65-
"fluent-json-schema": "^4.1.0",
63+
"fastify-jwt-jwks": "^1.1.4",
64+
"fastify-overview": "^3.6.0",
65+
"fastify-plugin": "^4.5.1",
66+
"fluent-json-schema": "^4.2.1",
6667
"prisma": "^4.15.0"
6768
},
6869
"devDependencies": {
6970
"@types/node": "^18.16.17",
7071
"@types/sinon": "^10.0.15",
7172
"@vitest/ui": "^0.31.4",
7273
"concurrently": "^7.6.0",
73-
"fastify-tsconfig": "^1.0.1",
74+
"fastify-tsconfig": "^2.0.0",
7475
"sinon": "^15.1.2",
7576
"tsup": "6.7.0",
7677
"tsx": "3.12.7",

apps/api/src/app.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload';
22
import fastifyEnv from '@fastify/env';
33
import {Type} from '@sinclair/typebox';
44
import {FastifyPluginAsync} from 'fastify';
5-
import fp from 'fastify-plugin';
65
import path, {join} from 'node:path';
76
import {fileURLToPath} from 'node:url';
87

apps/api/src/common/auth/auth0.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import '@fastify/jwt';
2+
import {
3+
FastifyInstance,
4+
FastifyPluginAsync,
5+
preHandlerHookHandler,
6+
} from 'fastify';
7+
import fastifyAuth0Verify, {Authenticate} from 'fastify-auth0-verify';
8+
import fp from 'fastify-plugin';
9+
import {AuthorizeOptions} from './authorize.js';
10+
11+
declare module '@fastify/jwt' {
12+
interface FastifyJWT {
13+
payload: {id: number}; // payload type is used for signing and verifying
14+
user: Record<string, unknown>; // user type is return type of `request.user` object
15+
}
16+
}
17+
18+
export function mockAuthProvider(context: {email: string}) {
19+
return fp(async (fastify: FastifyInstance) => {
20+
const auth0Authenticate: Authenticate = async req => {
21+
const email = context.email;
22+
const clientClaim = fastify.config.AUTH0_CLIENT_CLAIMS;
23+
const emailKey = `${clientClaim}/email`;
24+
req.user = {
25+
[emailKey]: email,
26+
};
27+
};
28+
29+
fastify.decorateRequest('user', null);
30+
fastify.decorate('authenticate', auth0Authenticate);
31+
});
32+
}
33+
34+
export const auth0Plugin: FastifyPluginAsync<{
35+
authProvider?: FastifyPluginAsync;
36+
}> = async (fastify, options) => {
37+
if (fastify.config.MOCK_AUTH) {
38+
await fastify.register(
39+
mockAuthProvider({
40+
email: fastify.config.MOCK_AUTH_EMAIL as string,
41+
}),
42+
);
43+
} else if (options.authProvider) {
44+
await fastify.register(options.authProvider);
45+
} else {
46+
await fastify.register(fastifyAuth0Verify.default, {
47+
domain: fastify.config.DOMAIN_AUTH0,
48+
secret: fastify.config.CLIENT_SECRET_AUTH,
49+
audience: fastify.config.AUDIENCE_AUTH0,
50+
});
51+
}
52+
53+
const authorize: (options?: AuthorizeOptions) => preHandlerHookHandler =
54+
(options = {mustBeAuthenticated: true}) =>
55+
async (req, reply) => {
56+
try {
57+
await fastify.authenticate(req, reply);
58+
} catch (e) {
59+
if (options.mustBeAuthenticated) {
60+
throw fastify.httpErrors.unauthorized();
61+
}
62+
}
63+
64+
const emailClaim = `${fastify.config.AUTH0_CLIENT_CLAIMS}/email`;
65+
66+
if (!req.user && options.mustBeAuthenticated) {
67+
throw fastify.httpErrors.unauthorized();
68+
}
69+
70+
const email = req.user[emailClaim] as string;
71+
72+
if (!email) {
73+
throw fastify.httpErrors.badRequest('No valid user data');
74+
}
75+
76+
const user = await fastify.prisma.user.findFirst({
77+
where: {
78+
email,
79+
},
80+
});
81+
82+
if (!user) {
83+
req.appUser = await fastify.prisma.user.create({
84+
data: {
85+
email,
86+
},
87+
});
88+
} else {
89+
req.appUser = user;
90+
}
91+
};
92+
93+
fastify.decorate('verifyAuth0', authorize);
94+
};
95+
96+
declare module 'fastify' {
97+
interface FastifyInstance {
98+
verifyAuth0: (options?: AuthorizeOptions) => preHandlerHookHandler;
99+
}
100+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface AuthorizeOptions {
2+
mustBeAuthenticated: boolean;
3+
}

apps/api/src/plugins/multi-auth.ts renamed to apps/api/src/common/auth/multiAuth.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import fastifyAuth from '@fastify/auth';
2-
import {preHandlerHookHandler} from 'fastify';
3-
import fp from 'fastify-plugin';
2+
import {FastifyPluginAsync, preHandlerHookHandler} from 'fastify';
3+
import {AuthorizeOptions} from './authorize.js';
44

5-
interface AuthorizeOptions {
6-
mustBeAuthenticated: boolean;
7-
}
8-
export default fp(async fastify => {
5+
export const multiAuthProviderPlugin: FastifyPluginAsync = async fastify => {
96
fastify.register(fastifyAuth);
107

11-
const preHookHandler: (options: AuthorizeOptions) => preHandlerHookHandler = (
12-
options = {mustBeAuthenticated: true},
13-
) =>
8+
const preHookHandler: (
9+
options?: AuthorizeOptions,
10+
) => preHandlerHookHandler = (options = {mustBeAuthenticated: true}) =>
1411
function (request, reply, done) {
1512
return fastify
1613
.auth([
@@ -21,7 +18,7 @@ export default fp(async fastify => {
2118
};
2219

2320
fastify.decorate('authorize', preHookHandler);
24-
});
21+
};
2522

2623
declare module 'fastify' {
2724
interface FastifyInstance {
Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import {tenant} from '@teamhanko/passkeys-sdk';
2-
import {preHandlerHookHandler} from 'fastify';
3-
import fp from 'fastify-plugin';
2+
import {FastifyPluginAsync, preHandlerHookHandler} from 'fastify';
43

54
interface AuthorizeOptions {
65
mustBeAuthenticated: boolean;
76
}
87

9-
export const passkeysPlugin = fp(async fastify => {
8+
export const passkeysPlugin: FastifyPluginAsync = async fastify => {
109
const passkeysApi = tenant({
1110
tenantId: fastify.config.HANKO_PASSKEYS_TENANT_ID,
1211
apiKey: fastify.config.HANKO_PASSKEYS_API_KEY,
@@ -15,9 +14,9 @@ export const passkeysPlugin = fp(async fastify => {
1514

1615
fastify.decorate('passkeysApi', passkeysApi);
1716

18-
const verify: (options: AuthorizeOptions) => preHandlerHookHandler =
17+
const verify: (options?: AuthorizeOptions) => preHandlerHookHandler =
1918
(options = {mustBeAuthenticated: true}) =>
20-
async (req, reply, done) => {
19+
async req => {
2120
const token = req.headers.authorization
2221
?.split('Bearer ')[1]
2322
.split('.')[1] as string;
@@ -31,26 +30,21 @@ export const passkeysPlugin = fp(async fastify => {
3130
});
3231

3332
if (user) {
34-
console.log('augment request with user', user);
3533
req.appUser = user;
36-
req.appUserOptional = user;
37-
done();
3834
} else if (options.mustBeAuthenticated) {
3935
throw fastify.httpErrors.unauthorized();
4036
}
4137
};
4238

4339
fastify.decorate('verifyHankoPasskey', verify);
44-
});
40+
};
4541

4642
export default passkeysPlugin;
4743

4844
declare module 'fastify' {
4945
interface FastifyInstance {
5046
passkeysApi: ReturnType<typeof tenant>;
5147

52-
verifyHankoPasskey: (
53-
options?: AuthorizeOptions,
54-
) => (req: FastifyRequest, reply: FastifyReply) => void;
48+
verifyHankoPasskey: (options?: AuthorizeOptions) => preHandlerHookHandler;
5549
}
5650
}

apps/api/src/common/auth/user.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {FastifyPluginAsync} from 'fastify';
2+
import {User} from '@codeimage/prisma-models';
3+
4+
export const appUserPlugin: FastifyPluginAsync = async fastify => {
5+
fastify.decorateRequest('appUser', null);
6+
};
7+
8+
declare module 'fastify' {
9+
interface FastifyRequest {
10+
appUser: User;
11+
}
12+
}

apps/api/src/plugins/auth0.ts

Lines changed: 0 additions & 111 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import fp from 'fastify-plugin';
2+
import {auth0Plugin} from '../common/auth/auth0.js';
3+
import {multiAuthProviderPlugin} from '../common/auth/multiAuth.js';
4+
import passkeysPlugin from '../common/auth/passkeys.js';
5+
import {appUserPlugin} from '../common/auth/user.js';
6+
7+
export default fp(
8+
async fastify => {
9+
fastify
10+
.register(fp(appUserPlugin))
11+
.register(fp(passkeysPlugin))
12+
.register(fp(auth0Plugin))
13+
.register(fp(multiAuthProviderPlugin));
14+
},
15+
{encapsulate: false},
16+
);

0 commit comments

Comments
 (0)