Expose `createToken` from `simple-oauth2`
Prerequisites
- [x] I have written a descriptive issue title
- [x] I have searched existing issues to ensure the feature has not already been requested
🚀 Feature Proposal
Expose createToken from simple-oauth2's AuthorizationCode (accessible currently via OAuth2Namespace#oauth2#createToken) directly on OAuth2Namespace.
Also, expose a method with correct typing for storing a primitive version of OAuth2Token#token, which can be stored in JSON environments and then passed back into createToken to be restored.
Motivation
👋 When using this with @fastify/secure-session it is quite useful to be able to store the inner OAuth2Token#token in the session itself, and then restore it back into a full OAuth2Token by calling OAuth2Namespace#oauth2#createToken, but it'd be nice to have this method exposed directly in OAuth2Namespace with the correct fastify-oauth2 types instead of the underlying simple-oauth2 types.
Example
import fastify from "fastify";
import fastifyOauth2 from "@fastify/oauth2";
import fastifySecureSession from "@fastify/secure-session";
if (!process.env.TWITCH_CLIENT_ID)
throw new Error("TWITCH_CLIENT_ID is required");
if (!process.env.TWITCH_CLIENT_SECRET)
throw new Error("TWITCH_CLIENT_SECRET is required");
if (!process.env.SESSION_SECRET) throw new Error("SESSION_SECRET is required");
const server = fastify();
const tokenRequestParams = {
client_id: process.env.TWITCH_CLIENT_ID,
client_secret: process.env.TWITCH_CLIENT_SECRET,
};
server.register(fastifyOauth2, {
name: "twitchOauth2",
scope: ["openid"],
credentials: {
client: {
id: process.env.TWITCH_CLIENT_ID,
secret: process.env.TWITCH_CLIENT_SECRET,
},
},
tokenRequestParams,
discovery: {
issuer: "https://id.twitch.tv/oauth2",
},
callbackUri: (req) => `${req.protocol}://${req.host}/login/twitch/callback`,
});
server.register(fastifySecureSession, {
key: Buffer.from(process.env.SESSION_SECRET, 'hex'),
cookie: {
path: '/',
httpOnly: true,
},
});
server.addHook("preHandler", async (req) => {
// Get the primitive token from the session and restore it as a full token
const tokenData = req.session.get("token");
const token = tokenData && server.twitchOauth2.oauth2.createToken(tokenData);
if (!token) return;
if (token.expired()) {
try {
const newToken = await token.refresh(tokenRequestParams);
req.session.set('token', { ...newToken.token, expires_at: newToken.token.expires_at.toISOString() });
} catch (error) {
console.error(`${req.id} failed to refresh token`, error);
req.session.regenerate();
}
return;
}
});
server.get("/login/twitch/callback", async (req, reply) => {
req.session.regenerate();
try {
const token = await server.twitchOauth2.getAccessTokenFromAuthorizationCodeFlow(req);
// Store the primitive token in the secure session
req.session.set('token', { ...token.token, expires_at: token.token.expires_at.toISOString() });
return reply.send(token.token);
} catch (error) {
console.error(`${req.id} failed to authenticate`, error);
return reply.send(new Error("Failed to authenticate"));
}
});
server.get("/login/twitch", async (req, reply) => {
req.session.regenerate();
const uri = await server.twitchOauth2.generateAuthorizationUri(req, reply);
return reply.redirect(uri);
});
server.listen({ port: 3000 }).then((res) => {
console.log(`Server running on ${res.replace("[::1]", "localhost")}`);
});
I have the exact same use-case.
but it'd be nice to have this method exposed directly in OAuth2Namespace with the correct fastify-oauth2 types instead of the underlying simple-oauth2 types.
Could you elaborate? What is problem we are solving here? Are we getting a shortcut?
-server.twitchOauth2.oauth2.createToken(tokenData);
+server.twitchOauth2.createToken(tokenData)
ATM I can't see the PROs on exposing an additional shortcut interface: what if simple-oauth2 changes the API, we need to update our code?
Really the pro is that it'd be typed correctly out of the box -- oauth2 is untyped at present, and createToken within simple-oauth2 does not return the same type as a token generated by this wrapper. It'd also be nice to see a serializable type/method for a token for storing in secure-session, as an aside.
I'm currently doing this to get roughly the right types:
import type { OAuth2Namespace, OAuth2Token, Token } from "@fastify/oauth2";
import type { AuthorizationCode } from "simple-oauth2";
interface TwitchOAuth2 extends OAuth2Namespace {
oauth2: AuthorizationCode;
}
declare module "fastify" {
interface FastifyInstance {
twitchOauth2: TwitchOAuth2;
}
}
type JSONToken = {
[key in keyof Token]: Token[key] extends Date ? string : Token[key];
};
declare module "@fastify/secure-session" {
interface SessionData {
token: JSONToken;
}
}
declare module "simple-oauth2" {
interface AuthorizationCode {
createToken(token: JSONToken): OAuth2Token;
}
}