Fix onboarding OAuth callback glitch (#1079)

This commit is contained in:
Alex Johansson 2021-11-03 10:47:52 +00:00 committed by GitHub
parent d147772d91
commit a002b194da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 66 additions and 33 deletions

View file

@ -1,3 +1,4 @@
import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types";
import { useState } from "react"; import { useState } from "react";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
@ -13,8 +14,14 @@ export default function ConnectIntegration(props: {
}) { }) {
const { type } = props; const { type } = props;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const mutation = useMutation(async () => { const mutation = useMutation(async () => {
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add"); const state: IntegrationOAuthCallbackState = {
returnTo: location.pathname + location.search,
};
const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}`;
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add" + searchParams);
if (!res.ok) { if (!res.ok) {
throw new Error("Something went wrong"); throw new Error("Something went wrong");
} }

View file

@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { encodeOAuthState } from "../utils";
const credentials = process.env.GOOGLE_API_CREDENTIALS!; const credentials = process.env.GOOGLE_API_CREDENTIALS!;
const scopes = [ const scopes = [
"https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.readonly",
@ -32,6 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// setting the prompt to 'consent' will force this consent // setting the prompt to 'consent' will force this consent
// every time, forcing a refresh_token to be returned. // every time, forcing a refresh_token to be returned.
prompt: "consent", prompt: "consent",
state: encodeOAuthState(req),
}); });
res.status(200).json({ url: authUrl }); res.status(200).json({ url: authUrl });

View file

@ -4,6 +4,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { decodeOAuthState } from "../utils";
const credentials = process.env.GOOGLE_API_CREDENTIALS; const credentials = process.env.GOOGLE_API_CREDENTIALS;
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -29,6 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback"; const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const token = await oAuth2Client.getToken(code); const token = await oAuth2Client.getToken(code);
const key = token.res?.data; const key = token.res?.data;
await prisma.credential.create({ await prisma.credential.create({
data: { data: {
@ -37,6 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: session.user.id, userId: session.user.id,
}, },
}); });
const state = decodeOAuthState(req);
res.redirect("/integrations"); res.redirect(state?.returnTo ?? "/integrations");
} }

View file

@ -2,42 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma"; import { encodeOAuthState } from "../utils";
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"]; const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
function generateAuthUrl() {
return (
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" +
scopes.join(" ") +
"&client_id=" +
process.env.MS_GRAPH_CLIENT_ID +
"&redirect_uri=" +
process.env.BASE_URL +
"/api/integrations/office365calendar/callback"
);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") { if (req.method === "GET") {
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { if (!session?.user) {
res.status(401).json({ message: "You must be logged in to do this" }); res.status(401).json({ message: "You must be logged in to do this" });
return; return;
} }
// Get user const state = encodeOAuthState(req);
await prisma.user.findFirst({ let url =
where: { "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" +
email: session.user.email, scopes.join(" ") +
}, "&client_id=" +
select: { process.env.MS_GRAPH_CLIENT_ID +
id: true, "&redirect_uri=" +
}, process.env.BASE_URL +
}); "/api/integrations/office365calendar/callback";
if (state) {
res.status(200).json({ url: generateAuthUrl() }); url += "&state=" + encodeURIComponent(state);
}
res.status(200).json({ url });
} }
} }

View file

@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma"; import prisma from "../../../../lib/prisma";
import { decodeOAuthState } from "../utils";
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"]; const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
@ -11,23 +12,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Check that user is authenticated // Check that user is authenticated
const session = await getSession({ req: req }); const session = await getSession({ req: req });
if (!session) { if (!session?.user?.id) {
res.status(401).json({ message: "You must be logged in to do this" }); res.status(401).json({ message: "You must be logged in to do this" });
return; return;
} }
if (typeof code !== "string") {
res.status(400).json({ message: "No code returned" });
return;
}
const toUrlEncoded = (payload) => const toUrlEncoded = (payload: Record<string, string>) =>
Object.keys(payload) Object.keys(payload)
.map((key) => key + "=" + encodeURIComponent(payload[key])) .map((key) => key + "=" + encodeURIComponent(payload[key]))
.join("&"); .join("&");
const body = toUrlEncoded({ const body = toUrlEncoded({
client_id: process.env.MS_GRAPH_CLIENT_ID, client_id: process.env.MS_GRAPH_CLIENT_ID!,
grant_type: "authorization_code", grant_type: "authorization_code",
code, code,
scope: scopes.join(" "), scope: scopes.join(" "),
redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback", redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback",
client_secret: process.env.MS_GRAPH_CLIENT_SECRET, client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
}); });
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
@ -62,5 +67,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
}); });
return res.redirect("/integrations"); const state = decodeOAuthState(req);
return res.redirect(state?.returnTo ?? "/integrations");
} }

3
pages/api/integrations/types.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export type IntegrationOAuthCallbackState = {
returnTo: string;
};

View file

@ -0,0 +1,21 @@
import { NextApiRequest } from "next";
import { IntegrationOAuthCallbackState } from "./types";
export function encodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
return undefined;
}
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
return JSON.stringify(state);
}
export function decodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
return undefined;
}
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
return state;
}