Skip to content

Expose createToken from simple-oauth2 #293

@MattIPv4

Description

@MattIPv4

Prerequisites

  • I have written a descriptive issue title
  • 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")}`);
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions