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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions