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 { useMutation } from "react-query";
@ -13,8 +14,14 @@ export default function ConnectIntegration(props: {
}) {
const { type } = props;
const [isLoading, setIsLoading] = useState(false);
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) {
throw new Error("Something went wrong");
}

View File

@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
import { encodeOAuthState } from "../utils";
const credentials = process.env.GOOGLE_API_CREDENTIALS!;
const scopes = [
"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
// every time, forcing a refresh_token to be returned.
prompt: "consent",
state: encodeOAuthState(req),
});
res.status(200).json({ url: authUrl });

View File

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

View File

@ -2,42 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
import prisma from "../../../../lib/prisma";
import { encodeOAuthState } from "../utils";
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) {
if (req.method === "GET") {
// Check that user is authenticated
const session = await getSession({ req: req });
if (!session) {
if (!session?.user) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
// Get user
await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
},
});
res.status(200).json({ url: generateAuthUrl() });
const state = encodeOAuthState(req);
let url =
"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";
if (state) {
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 prisma from "../../../../lib/prisma";
import { decodeOAuthState } from "../utils";
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
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" });
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)
.map((key) => key + "=" + encodeURIComponent(payload[key]))
.join("&");
const body = toUrlEncoded({
client_id: process.env.MS_GRAPH_CLIENT_ID,
client_id: process.env.MS_GRAPH_CLIENT_ID!,
grant_type: "authorization_code",
code,
scope: scopes.join(" "),
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", {
@ -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;
}