strict state for oauth2
This commit is contained in:
parent
0ed1a3072d
commit
4588e1c1bf
@ -2,4 +2,6 @@
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background-color: #fff;
|
||||
min-height: calc(100vh - 54px);
|
||||
}
|
||||
|
@ -24,6 +24,10 @@
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
|
||||
a {
|
||||
background-color: #00aaff;
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
// The stylesheet is for Univia-pro font from Adobe
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
|
@ -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('/');
|
||||
}
|
||||
}
|
||||
|
@ -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 { 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()}`);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user