Skip to content

Commit 47a8e4a

Browse files
feat(auth): support legacy authentication
1 parent 430218a commit 47a8e4a

File tree

3 files changed

+89
-28
lines changed

3 files changed

+89
-28
lines changed

src/plugins/auth.ts

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fp from "fastify-plugin";
66
import jwt, { JwtHeader, SigningKeyCallback } from "jsonwebtoken";
77
import { JwksClient } from "jwks-rsa";
88
import * as client from "openid-client";
9+
import { skipSubjectCheck, WWWAuthenticateChallengeError } from "openid-client";
910

1011
export interface AuthPluginOptions {
1112
/** The discovery URL of the OpenID Connect provider. */
@@ -52,6 +53,15 @@ export const AuthResponseSchema: ResponseSchema = {
5253
),
5354
};
5455

56+
class UnauthorizedError extends Error {
57+
cause: Error;
58+
constructor(cause: Error) {
59+
super();
60+
this.cause = cause;
61+
this.name = "UnauthorizedError";
62+
}
63+
}
64+
5565
/**
5666
* The Auth plugin adds authentication ability to the Fastify instance.
5767
*
@@ -66,21 +76,28 @@ export default fp<AuthPluginOptions>(async (fastify, opts) => {
6676
fastify.log.warn("Skip Auth: ON");
6777
}
6878

69-
const key = await (async () => {
79+
const config = await (async () => {
7080
if (skip) {
7181
return null;
7282
} else {
73-
const config = await client.discovery(
83+
return await client.discovery(
7484
new URL(opts.authDiscoveryURL),
7585
opts.authClientID,
7686
);
87+
}
88+
})();
89+
90+
const key = await (async () => {
91+
if (skip) {
92+
return null;
93+
} else {
7794
fastify.log.info(
78-
{ opts, metadata: config?.serverMetadata() },
95+
{ opts, metadata: config!.serverMetadata() },
7996
"Successfully discovered the OpenID Connect provider.",
8097
);
8198

8299
const jwksClient = new JwksClient({
83-
jwksUri: config.serverMetadata().jwks_uri ?? "",
100+
jwksUri: config!.serverMetadata().jwks_uri ?? "",
84101
});
85102
return async (header: JwtHeader, callback: SigningKeyCallback) => {
86103
jwksClient.getSigningKey(header.kid, (err, key) => {
@@ -91,9 +108,47 @@ export default fp<AuthPluginOptions>(async (fastify, opts) => {
91108
}
92109
})();
93110

111+
async function verify(token: string) {
112+
try {
113+
const info = await new Promise<jwt.JwtPayload>((resolve, reject) => {
114+
jwt.verify(token, key!, (err, info) => {
115+
if (err) {
116+
return reject(err);
117+
}
118+
resolve(info as jwt.JwtPayload);
119+
});
120+
});
121+
return info.email && getUsernameFromEmail(info.email);
122+
} catch (e) {
123+
if (
124+
e instanceof jwt.JsonWebTokenError ||
125+
e instanceof jwt.TokenExpiredError ||
126+
e instanceof jwt.NotBeforeError
127+
) {
128+
throw new UnauthorizedError(e);
129+
}
130+
throw e;
131+
}
132+
}
133+
134+
async function verifyLegacy(token: string) {
135+
try {
136+
const info = await client.fetchUserInfo(config!, token, skipSubjectCheck);
137+
return info.email && getUsernameFromEmail(info.email);
138+
} catch (e) {
139+
if (
140+
e instanceof WWWAuthenticateChallengeError &&
141+
e.response?.status === 401
142+
) {
143+
throw new UnauthorizedError(new Error(await e.response.text()));
144+
}
145+
throw e;
146+
}
147+
}
148+
94149
fastify.decorateRequest("user", undefined);
95150
fastify.decorate(
96-
"auth",
151+
"authPlugin",
97152
async function (request: FastifyRequest, reply: FastifyReply) {
98153
if (skip) return;
99154
if (!key) return;
@@ -115,23 +170,24 @@ export default fp<AuthPluginOptions>(async (fastify, opts) => {
115170
}
116171

117172
try {
118-
const info = await new Promise<jwt.JwtPayload>((resolve, reject) => {
119-
jwt.verify(token, key, (err, info) => {
120-
if (err) {
121-
return reject(err);
122-
}
123-
resolve(info as jwt.JwtPayload);
124-
});
173+
// Verify the token and set the user in the request.
174+
// If the token cannot be verified by the modern method,
175+
// fall back to the legacy method.
176+
request.user = await verify(token).catch(async (e) => {
177+
if (e instanceof UnauthorizedError) {
178+
fastify.log.debug(
179+
"Modern verification failed, falling back to legacy method.",
180+
);
181+
return await verifyLegacy(token).catch(() => {
182+
throw e;
183+
});
184+
}
185+
throw e;
125186
});
126-
request.user = info.email && getUsernameFromEmail(info.email);
127187
} catch (e) {
128-
console.log(e);
129-
if (
130-
e instanceof jwt.JsonWebTokenError ||
131-
e instanceof jwt.TokenExpiredError ||
132-
e instanceof jwt.NotBeforeError
133-
) {
134-
return reply.status(401).send(`${e.name}: ${e.message}`);
188+
if (e instanceof UnauthorizedError) {
189+
const cause = e.cause;
190+
return reply.status(401).send(`${cause.name}: ${cause.message}`);
135191
}
136192
throw e;
137193
}
@@ -140,12 +196,13 @@ export default fp<AuthPluginOptions>(async (fastify, opts) => {
140196
});
141197

142198
function getUsernameFromEmail(email: string): string {
143-
return email.split("@")[0];
199+
const [username] = email.split("@");
200+
return username;
144201
}
145202

146203
declare module "fastify" {
147204
export interface FastifyInstance {
148-
auth(request: FastifyRequest, reply: FastifyReply): Promise<void>;
205+
authPlugin(request: FastifyRequest, reply: FastifyReply): Promise<void>;
149206
}
150207

151208
export interface FastifyRequest {

src/routes/auth-example/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const authExample: FastifyPluginAsync = async (
88
fastify: FastifyTypebox,
99
opts,
1010
): Promise<void> => {
11-
// Applying auth to this route
11+
// Applying authPlugin to this route
1212
fastify.get(
1313
"/",
1414
{
@@ -23,17 +23,17 @@ const authExample: FastifyPluginAsync = async (
2323
AuthResponseSchema,
2424
]),
2525
},
26-
preHandler: fastify.auth,
26+
preHandler: fastify.authPlugin,
2727
},
2828
async function (request, reply) {
2929
return `${request.user} is authenticated`;
3030
},
3131
);
3232

33-
// Applying auth to the prefix /sub-auth
33+
// Applying authPlugin to the prefix /sub-auth
3434
fastify.register(
3535
async function (fastify) {
36-
fastify.addHook("preHandler", fastify.auth);
36+
fastify.addHook("preHandler", fastify.authPlugin);
3737
fastify.get(
3838
"/",
3939
{

test/plugins/auth.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ await suite("auth plugin", async () => {
1616
});
1717
fastify.get(
1818
"/secret",
19-
{ preHandler: fastify.auth },
19+
{ preHandler: fastify.authPlugin },
2020
async (request) => request.user,
2121
);
2222
await fastify.ready();
@@ -98,7 +98,11 @@ await suite("auth plugin with skipping", async () => {
9898
authClientID: "",
9999
authSkip: true,
100100
});
101-
fastify.get("/secret", { preHandler: fastify.auth }, async () => "ok");
101+
fastify.get(
102+
"/secret",
103+
{ preHandler: fastify.authPlugin },
104+
async () => "ok",
105+
);
102106
await fastify.ready();
103107
});
104108
afterEach(async () => {

0 commit comments

Comments
 (0)