strict state for oauth2

This commit is contained in:
Evert Prants 2022-08-29 21:34:46 +03:00
parent 0ed1a3072d
commit 4588e1c1bf
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
8 changed files with 107 additions and 19 deletions

View File

@ -2,4 +2,6 @@
max-width: 1080px;
margin: 0 auto;
padding: 1rem;
background-color: #fff;
min-height: calc(100vh - 54px);
}

View File

@ -24,6 +24,10 @@
&.active {
font-weight: bold;
a {
background-color: #00aaff;
}
}
a {

47
lib/utils/crypto.ts Normal file
View 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();
};

View File

@ -1,5 +1,5 @@
import { Head, Html, Main, NextScript } from 'next/document';
// The stylesheet is for Univia-pro font from Adobe
export default function Document() {
return (
<Html>

View File

@ -2,23 +2,48 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { getAccessToken } from '../../lib/api/remote';
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(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.query.code) {
// TODO: parse state
if (!req.query.state) {
return res.redirect('/');
}
const getAuth = await getAccessToken(req.query.code as string);
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
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, {
expires: new Date(Date.now() + getAuth.expires_in * 1000),
secure: process.env.NODE_ENV === 'production',
signed: true,
});
cookies.set('validation', undefined, {
expires: new Date(0),
secure: process.env.NODE_ENV === 'production',
signed: true,
});
}
res.redirect('/');
}
}

View File

@ -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' })
}

View File

@ -1,15 +1,37 @@
import Cookies from 'cookies';
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) {
const stateToken = generateString(16);
const state = encrypt(
JSON.stringify({
redirect_uri: redirect,
state: stateToken,
})
);
const params = new URLSearchParams({
client_id: CLIENT_ID,
response_type: 'code',
redirect_uri: `${PUBLIC_URL}/api/callback`,
redirect_uri: redirect,
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()}`);
}

View File

@ -4,6 +4,7 @@ body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: rgb(231, 231, 231);
}
a {