strict state for oauth2
This commit is contained in:
parent
0ed1a3072d
commit
4588e1c1bf
@ -2,4 +2,6 @@
|
|||||||
max-width: 1080px;
|
max-width: 1080px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: calc(100vh - 54px);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: #00aaff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
47
lib/utils/crypto.ts
Normal file
47
lib/utils/crypto.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import { CLIENT_SECRET } from '../constants';
|
||||||
|
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
const ALGORITHM = 'aes-256-cbc';
|
||||||
|
|
||||||
|
export const generateString = (length: number): string =>
|
||||||
|
crypto.randomBytes(length).toString('hex').slice(0, length);
|
||||||
|
|
||||||
|
// https://stackoverflow.com/q/52212430
|
||||||
|
/**
|
||||||
|
* Symmetric encryption function
|
||||||
|
* @param value String to encrypt
|
||||||
|
* @returns Encrypted text
|
||||||
|
*/
|
||||||
|
export const encrypt = (value: string) => {
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv(
|
||||||
|
ALGORITHM,
|
||||||
|
Buffer.from(CLIENT_SECRET, 'hex'),
|
||||||
|
iv
|
||||||
|
);
|
||||||
|
let encrypted = cipher.update(value);
|
||||||
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
|
return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symmetric decryption function
|
||||||
|
* @param text Encrypted string
|
||||||
|
* @returns Decrypted text
|
||||||
|
*/
|
||||||
|
export const decrypt = (text: string) => {
|
||||||
|
const [iv, encryptedText] = text
|
||||||
|
.split(':')
|
||||||
|
.map((part) => Buffer.from(part, 'hex'));
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
ALGORITHM,
|
||||||
|
Buffer.from(CLIENT_SECRET, 'hex'),
|
||||||
|
iv
|
||||||
|
);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encryptedText);
|
||||||
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
|
return decrypted.toString();
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { Head, Html, Main, NextScript } from 'next/document';
|
import { Head, Html, Main, NextScript } from 'next/document';
|
||||||
// The stylesheet is for Univia-pro font from Adobe
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
|
@ -2,23 +2,48 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { getAccessToken } from '../../lib/api/remote';
|
import { getAccessToken } from '../../lib/api/remote';
|
||||||
|
|
||||||
import Cookies from 'cookies';
|
import Cookies from 'cookies';
|
||||||
import { COOKIE_KEYS } from '../../lib/constants';
|
import { COOKIE_KEYS, PUBLIC_URL } from '../../lib/constants';
|
||||||
|
import { decrypt } from '../../lib/utils/crypto';
|
||||||
|
|
||||||
|
const redirect = `${PUBLIC_URL}/api/callback`;
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
if (req.query.code) {
|
if (req.query.code) {
|
||||||
// TODO: parse state
|
if (!req.query.state) {
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
const getAuth = await getAccessToken(req.query.code as string);
|
const getAuth = await getAccessToken(req.query.code as string);
|
||||||
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
|
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
|
||||||
|
|
||||||
if (getAuth) {
|
if (getAuth) {
|
||||||
|
const decrypted = decrypt(req.query.state as string);
|
||||||
|
const stateToken = cookies.get('validation', { signed: true });
|
||||||
|
const parsedState = JSON.parse(decrypted);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedState.state !== stateToken ||
|
||||||
|
parsedState.redirect_uri !== redirect
|
||||||
|
) {
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
cookies.set('authorization', getAuth.access_token, {
|
cookies.set('authorization', getAuth.access_token, {
|
||||||
expires: new Date(Date.now() + getAuth.expires_in * 1000),
|
expires: new Date(Date.now() + getAuth.expires_in * 1000),
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
signed: true,
|
signed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cookies.set('validation', undefined, {
|
||||||
|
expires: new Date(0),
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
signed: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
type Data = {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
res.status(200).json({ name: 'John Doe' })
|
|
||||||
}
|
|
@ -1,15 +1,37 @@
|
|||||||
|
import Cookies from 'cookies';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { CLIENT_ID, OAUTH_URL, PUBLIC_URL } from '../../lib/constants';
|
import {
|
||||||
|
CLIENT_ID,
|
||||||
|
COOKIE_KEYS,
|
||||||
|
OAUTH_URL,
|
||||||
|
PUBLIC_URL,
|
||||||
|
} from '../../lib/constants';
|
||||||
|
import { encrypt, generateString } from '../../lib/utils/crypto';
|
||||||
|
|
||||||
|
const redirect = `${PUBLIC_URL}/api/callback`;
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const stateToken = generateString(16);
|
||||||
|
const state = encrypt(
|
||||||
|
JSON.stringify({
|
||||||
|
redirect_uri: redirect,
|
||||||
|
state: stateToken,
|
||||||
|
})
|
||||||
|
);
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: CLIENT_ID,
|
client_id: CLIENT_ID,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
redirect_uri: `${PUBLIC_URL}/api/callback`,
|
redirect_uri: redirect,
|
||||||
scope: 'management',
|
scope: 'management',
|
||||||
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: generate state
|
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
|
||||||
|
|
||||||
|
cookies.set('validation', stateToken, {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
signed: true,
|
||||||
|
});
|
||||||
|
|
||||||
res.redirect(`${OAUTH_URL}/authorize?${params.toString()}`);
|
res.redirect(`${OAUTH_URL}/authorize?${params.toString()}`);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
background-color: rgb(231, 231, 231);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
Reference in New Issue
Block a user