Add /api/website/allow endpoint
This commit is contained in:
parent
9aa8ee6105
commit
ca78ddc078
14 changed files with 305 additions and 126 deletions
|
@ -2,7 +2,9 @@
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||||
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
|
|
5
backend/package-lock.json
generated
5
backend/package-lock.json
generated
|
@ -1082,6 +1082,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||||
},
|
},
|
||||||
|
"base64url": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
|
||||||
|
},
|
||||||
"binary-extensions": {
|
"binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
"@types/url-join": "^4.0.0",
|
"@types/url-join": "^4.0.0",
|
||||||
"@wulkanowy/sdk": "^0.1.1",
|
"@wulkanowy/sdk": "^0.1.1",
|
||||||
"apollo-server-fastify": "^2.19.2",
|
"apollo-server-fastify": "^2.19.2",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
|
"date-fns": "^2.16.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"fastify": "^3.10.1",
|
"fastify": "^3.10.1",
|
||||||
"fastify-cookie": "^5.1.0",
|
"fastify-cookie": "^5.1.0",
|
||||||
|
|
52
backend/src/codes.ts
Normal file
52
backend/src/codes.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { addSeconds, isAfter } from 'date-fns';
|
||||||
|
import type { FastifyLoggerInstance } from 'fastify';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import type { CodeChallenge, CodeContent, CodeInfo } from './types';
|
||||||
|
import { createKey, encryptSymmetrical } from './utils';
|
||||||
|
|
||||||
|
const codes = new Map<string, CodeInfo>();
|
||||||
|
|
||||||
|
export function createCode(options: {
|
||||||
|
clientId: string;
|
||||||
|
scopes: string[];
|
||||||
|
studentIds: number[];
|
||||||
|
publicKey: string;
|
||||||
|
tokenKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPassword: string;
|
||||||
|
encryptedSDK: string;
|
||||||
|
codeChallenge?: CodeChallenge
|
||||||
|
}): string {
|
||||||
|
const expires = addSeconds(new Date(), 60);
|
||||||
|
const tokenSecret = createKey();
|
||||||
|
let id: string;
|
||||||
|
do {
|
||||||
|
id = nanoid();
|
||||||
|
} while (codes.has(id));
|
||||||
|
codes.set(id, {
|
||||||
|
expires,
|
||||||
|
id,
|
||||||
|
clientId: options.clientId,
|
||||||
|
scopes: options.scopes,
|
||||||
|
studentIds: options.studentIds,
|
||||||
|
publicKey: options.publicKey,
|
||||||
|
encryptedPrivateKey: options.encryptedPrivateKey,
|
||||||
|
encryptedSDK: options.encryptedSDK,
|
||||||
|
encryptedPassword: options.encryptedPassword,
|
||||||
|
tokenSecret,
|
||||||
|
codeChallenge: options.codeChallenge,
|
||||||
|
});
|
||||||
|
const content: CodeContent = {
|
||||||
|
tk: options.tokenKey,
|
||||||
|
};
|
||||||
|
return `${id}~${encryptSymmetrical(JSON.stringify(content), tokenSecret)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanUpCodes(logger: FastifyLoggerInstance): void {
|
||||||
|
codes.forEach((code) => {
|
||||||
|
if (isAfter(new Date(), code.expires)) {
|
||||||
|
codes.delete(code.id);
|
||||||
|
logger.info(`Code ${code.id} expired`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import FastifyCookie from 'fastify-cookie';
|
||||||
import FastifyHttpProxy from 'fastify-http-proxy';
|
import FastifyHttpProxy from 'fastify-http-proxy';
|
||||||
import FastifySensible from 'fastify-sensible';
|
import FastifySensible from 'fastify-sensible';
|
||||||
import FastifySession from 'fastify-session';
|
import FastifySession from 'fastify-session';
|
||||||
|
import { cleanUpCodes } from './codes';
|
||||||
import { websitePrefix } from './constants';
|
import { websitePrefix } from './constants';
|
||||||
import database from './database/database';
|
import database from './database/database';
|
||||||
import registerOAuth from './routes/oauth2';
|
import registerOAuth from './routes/oauth2';
|
||||||
|
@ -48,6 +49,10 @@ async function start() {
|
||||||
const port = parseIntStrict(requireEnv('PORT'));
|
const port = parseIntStrict(requireEnv('PORT'));
|
||||||
await server.listen(port);
|
await server.listen(port);
|
||||||
server.log.info(`Listening on port ${port}`);
|
server.log.info(`Listening on port ${port}`);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
cleanUpCodes(server.log);
|
||||||
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
start()
|
start()
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { MyFastifyInstance, StudentsMode } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKey,
|
createKey,
|
||||||
getSessionData, isObject, parseScopeParam, validateOptionalParam, validateParam,
|
getSessionData, isObject, parseArrayParam, validateOptionalParam, validateParam,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
|
|
||||||
export default function registerAuthorize(server: MyFastifyInstance): void {
|
export default function registerAuthorize(server: MyFastifyInstance): void {
|
||||||
|
@ -76,7 +76,7 @@ export default function registerAuthorize(server: MyFastifyInstance): void {
|
||||||
}
|
}
|
||||||
const studentsMode = request.query.students_mode as StudentsMode;
|
const studentsMode = request.query.students_mode as StudentsMode;
|
||||||
|
|
||||||
const requestedScopes = _.uniq(parseScopeParam('scope', request.query.scope));
|
const requestedScopes = _.uniq(parseArrayParam('scope', request.query.scope));
|
||||||
requestedScopes.forEach((scope) => {
|
requestedScopes.forEach((scope) => {
|
||||||
if (!scopes.includes(scope)) {
|
if (!scopes.includes(scope)) {
|
||||||
throw new ScopeError(`Unknown scope ${scope}`);
|
throw new ScopeError(`Unknown scope ${scope}`);
|
||||||
|
|
82
backend/src/routes/website-api/allow.ts
Normal file
82
backend/src/routes/website-api/allow.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { createCode } from '../../codes';
|
||||||
|
import { ParamError } from '../../errors';
|
||||||
|
import type { MyFastifyInstance, SerializedSDK } from '../../types';
|
||||||
|
import { StudentsMode } from '../../types';
|
||||||
|
import {
|
||||||
|
decryptSymmetrical, encryptSymmetrical,
|
||||||
|
getSessionData, isObject, parseArrayParam, parseIntStrict, validateParam,
|
||||||
|
} from '../../utils';
|
||||||
|
|
||||||
|
export default function registerAllow(server: MyFastifyInstance): void {
|
||||||
|
server.get('/allow', async (
|
||||||
|
request,
|
||||||
|
reply,
|
||||||
|
) => {
|
||||||
|
if (!isObject(request.query)) {
|
||||||
|
server.log.warn('Request query is not an object');
|
||||||
|
throw server.httpErrors.badRequest();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validateParam('prompt_id', request.query.prompt_id);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ParamError) {
|
||||||
|
throw server.httpErrors.badRequest(error.message);
|
||||||
|
}
|
||||||
|
server.log.error(error);
|
||||||
|
throw server.httpErrors.internalServerError();
|
||||||
|
}
|
||||||
|
const encryptedTokenKey: string | undefined = request.cookies[`etk-${request.query.prompt_id}`];
|
||||||
|
if (!encryptedTokenKey) throw server.httpErrors.badRequest('Missing etk cookie');
|
||||||
|
|
||||||
|
const sessionData = getSessionData(request.session);
|
||||||
|
const prompt = sessionData.prompts.get(request.query.prompt_id);
|
||||||
|
if (!prompt) throw server.httpErrors.badRequest('Prompt data not found');
|
||||||
|
if (!prompt.loginInfo) throw server.httpErrors.badRequest('Login data not provided');
|
||||||
|
|
||||||
|
const tokenKey = decryptSymmetrical(encryptedTokenKey, prompt.promptSecret);
|
||||||
|
const serializedSDK = JSON.parse(decryptSymmetrical(prompt.loginInfo.encryptedSDK, tokenKey)) as SerializedSDK;
|
||||||
|
|
||||||
|
let studentIds: number[] = [];
|
||||||
|
if (prompt.studentsMode !== StudentsMode.None) {
|
||||||
|
try {
|
||||||
|
studentIds = parseArrayParam('student_ids', request.query.student_ids).map(parseIntStrict);
|
||||||
|
if (studentIds.length === 0) throw new ParamError('student_ids should not be empty');
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ParamError) {
|
||||||
|
throw server.httpErrors.badRequest(error.message);
|
||||||
|
}
|
||||||
|
server.log.error(error);
|
||||||
|
throw server.httpErrors.internalServerError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSerializedSDK: SerializedSDK = {
|
||||||
|
client: serializedSDK.client,
|
||||||
|
diaries: serializedSDK.diaries.filter((diary) => studentIds.includes(diary.info.studentId)),
|
||||||
|
};
|
||||||
|
const newEncryptedSDK = encryptSymmetrical(JSON.stringify(newSerializedSDK), tokenKey);
|
||||||
|
|
||||||
|
const code = createCode({
|
||||||
|
studentIds,
|
||||||
|
scopes: prompt.scopes,
|
||||||
|
clientId: prompt.clientId,
|
||||||
|
publicKey: prompt.loginInfo.publicKey,
|
||||||
|
encryptedSDK: newEncryptedSDK,
|
||||||
|
encryptedPassword: prompt.loginInfo.encryptedPassword,
|
||||||
|
encryptedPrivateKey: prompt.loginInfo.encryptedPrivateKey,
|
||||||
|
tokenKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Find why the promise never resolves
|
||||||
|
reply.clearCookie(`etk-${request.query.prompt_id}`);
|
||||||
|
// In case execution of setCookie takes some time
|
||||||
|
// TODO: Remove
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
const redirectUri = new URL(prompt.redirectUri);
|
||||||
|
redirectUri.searchParams.set('code', code);
|
||||||
|
if (prompt.state) redirectUri.searchParams.set('state', prompt.state);
|
||||||
|
await reply.redirect(redirectUri.toString());
|
||||||
|
sessionData.prompts.delete(request.query.prompt_id);
|
||||||
|
});
|
||||||
|
}
|
35
backend/src/routes/website-api/deny.ts
Normal file
35
backend/src/routes/website-api/deny.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { ParamError } from '../../errors';
|
||||||
|
import type { MyFastifyInstance } from '../../types';
|
||||||
|
import { getSessionData, isObject, validateParam } from '../../utils';
|
||||||
|
|
||||||
|
export default function registerDeny(server: MyFastifyInstance): void {
|
||||||
|
server.get('/deny', async (
|
||||||
|
request,
|
||||||
|
reply,
|
||||||
|
) => {
|
||||||
|
if (!isObject(request.query)) {
|
||||||
|
server.log.warn('Request query is not an object');
|
||||||
|
throw server.httpErrors.badRequest();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validateParam('prompt_id', request.query.prompt_id);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ParamError) {
|
||||||
|
throw server.httpErrors.badRequest(error.message);
|
||||||
|
}
|
||||||
|
server.log.error(error);
|
||||||
|
throw server.httpErrors.internalServerError();
|
||||||
|
}
|
||||||
|
// TODO: Find why the promise never resolves
|
||||||
|
reply.clearCookie(`epk-${request.query.prompt_id}`);
|
||||||
|
// In case execution of setCookie takes some time
|
||||||
|
// TODO: Remove
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const sessionData = getSessionData(request.session);
|
||||||
|
const prompt = sessionData.prompts.get(request.query.prompt_id);
|
||||||
|
if (!prompt) throw server.httpErrors.badRequest('Prompt data not found');
|
||||||
|
await reply.redirect(`${prompt.redirectUri}?error=access_denied&error_description=${encodeURIComponent('User denied')}`);
|
||||||
|
sessionData.prompts.delete(request.query.prompt_id);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { ApolloServer } from 'apollo-server-fastify';
|
import { ApolloServer } from 'apollo-server-fastify';
|
||||||
import { buildSchema } from 'type-graphql';
|
import { buildSchema } from 'type-graphql';
|
||||||
import { ParamError } from '../../errors';
|
|
||||||
import type { ApolloContext, MyFastifyInstance } from '../../types';
|
import type { ApolloContext, MyFastifyInstance } from '../../types';
|
||||||
import { getSessionData, isObject, validateParam } from '../../utils';
|
import { getSessionData } from '../../utils';
|
||||||
|
import registerAllow from './allow';
|
||||||
|
import registerDeny from './deny';
|
||||||
import LoginResolver from './resolvers/login-resolver';
|
import LoginResolver from './resolvers/login-resolver';
|
||||||
import PromptInfoResolver from './resolvers/prompt-info-resolver';
|
import PromptInfoResolver from './resolvers/prompt-info-resolver';
|
||||||
import type { WebsiteAPIContext } from './types';
|
import type { WebsiteAPIContext } from './types';
|
||||||
|
@ -28,33 +29,6 @@ export default async function registerWebsiteApi(server: MyFastifyInstance): Pro
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.get('/deny', async (
|
registerDeny(server);
|
||||||
request,
|
registerAllow(server);
|
||||||
reply,
|
|
||||||
) => {
|
|
||||||
if (!isObject(request.query)) {
|
|
||||||
server.log.warn('Request query is not an object');
|
|
||||||
throw server.httpErrors.badRequest();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
validateParam('prompt_id', request.query.prompt_id);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ParamError) {
|
|
||||||
throw server.httpErrors.badRequest(error.message);
|
|
||||||
}
|
|
||||||
server.log.error(error);
|
|
||||||
throw server.httpErrors.internalServerError();
|
|
||||||
}
|
|
||||||
// TODO: Find why the promise never resolves
|
|
||||||
reply.clearCookie(`epk-${request.query.prompt_id}`);
|
|
||||||
// In case execution of setCookie takes some time
|
|
||||||
// TODO: Remove
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const sessionData = getSessionData(request.session);
|
|
||||||
const prompt = sessionData.prompts.get(request.query.prompt_id);
|
|
||||||
if (!prompt) throw server.httpErrors.badRequest('Prompt data not found');
|
|
||||||
await reply.redirect(`${prompt.redirectUri}?error=access_denied&error_description=${encodeURIComponent('User denied')}`);
|
|
||||||
sessionData.prompts.delete(request.query.prompt_id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from 'type-graphql';
|
} from 'type-graphql';
|
||||||
import type { SerializedSDK } from '../../../types';
|
import type { SerializedSDK } from '../../../types';
|
||||||
import {
|
import {
|
||||||
|
createKey,
|
||||||
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, isObject, verifyCaptchaResponse,
|
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, isObject, verifyCaptchaResponse,
|
||||||
} from '../../../utils';
|
} from '../../../utils';
|
||||||
import { CaptchaError, InvalidVulcanCredentialsError, UnknownPromptError } from '../errors';
|
import { CaptchaError, InvalidVulcanCredentialsError, UnknownPromptError } from '../errors';
|
||||||
|
@ -39,12 +40,6 @@ export default class LoginResolver {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const diaryList = await client.getDiaryList();
|
const diaryList = await client.getDiaryList();
|
||||||
const { privateKey, publicKey } = await generatePrivatePublicPair();
|
|
||||||
const encryptedPrivateKey = encryptSymmetrical(
|
|
||||||
privateKey,
|
|
||||||
prompt.promptSecret,
|
|
||||||
);
|
|
||||||
const encryptedPassword = encryptWithPublicKey(password, publicKey);
|
|
||||||
const diaryStudents = _.groupBy(diaryList.map((e) => e.serialized.info), 'studentId');
|
const diaryStudents = _.groupBy(diaryList.map((e) => e.serialized.info), 'studentId');
|
||||||
const students = _.toPairs(diaryStudents)
|
const students = _.toPairs(diaryStudents)
|
||||||
.map(([, diaryInfoList]: [string, DiaryInfo[]]) => diaryInfoList[0])
|
.map(([, diaryInfoList]: [string, DiaryInfo[]]) => diaryInfoList[0])
|
||||||
|
@ -56,9 +51,17 @@ export default class LoginResolver {
|
||||||
client: client.serialize(),
|
client: client.serialize(),
|
||||||
diaries: diaryList.map(({ serialized }) => serialized),
|
diaries: diaryList.map(({ serialized }) => serialized),
|
||||||
};
|
};
|
||||||
const encryptedSDK = encryptWithPublicKey(JSON.stringify(serializedSDK), publicKey);
|
|
||||||
|
const { privateKey, publicKey } = await generatePrivatePublicPair();
|
||||||
|
const tokenKey = createKey();
|
||||||
|
const encryptedSDK = encryptSymmetrical(JSON.stringify(serializedSDK), tokenKey);
|
||||||
|
const encryptedPassword = encryptWithPublicKey(password, publicKey);
|
||||||
|
const encryptedPrivateKey = encryptSymmetrical(privateKey, tokenKey);
|
||||||
|
const encryptedTokenKey = encryptSymmetrical(tokenKey, prompt.promptSecret);
|
||||||
|
|
||||||
prompt.loginInfo = {
|
prompt.loginInfo = {
|
||||||
encryptedPassword,
|
encryptedPassword,
|
||||||
|
encryptedPrivateKey,
|
||||||
encryptedSDK,
|
encryptedSDK,
|
||||||
publicKey,
|
publicKey,
|
||||||
host,
|
host,
|
||||||
|
@ -66,7 +69,7 @@ export default class LoginResolver {
|
||||||
availableStudentIds: students.map(({ studentId }) => studentId),
|
availableStudentIds: students.map(({ studentId }) => studentId),
|
||||||
};
|
};
|
||||||
// TODO: Find why the promise never resolves
|
// TODO: Find why the promise never resolves
|
||||||
reply.setCookie(`epk-${promptId}`, encryptedPrivateKey, {
|
reply.setCookie(`etk-${promptId}`, encryptedTokenKey, {
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
|
@ -21,22 +21,25 @@ registerEnumType(StudentsMode, {
|
||||||
name: 'StudentsMode',
|
name: 'StudentsMode',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface CodeChallenge {
|
||||||
|
value: string;
|
||||||
|
method: 'plain' | 'S256';
|
||||||
|
}
|
||||||
|
|
||||||
export interface Prompt {
|
export interface Prompt {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
scopes: string[],
|
scopes: string[],
|
||||||
state?: string;
|
state?: string;
|
||||||
codeChallenge?: {
|
codeChallenge?: CodeChallenge;
|
||||||
value: string;
|
|
||||||
method: 'plain' | 'S256';
|
|
||||||
};
|
|
||||||
studentsMode: StudentsMode;
|
studentsMode: StudentsMode;
|
||||||
promptSecret: Buffer;
|
promptSecret: string;
|
||||||
loginInfo?: {
|
loginInfo?: {
|
||||||
host: string;
|
host: string;
|
||||||
username: string;
|
username: string;
|
||||||
encryptedPassword: string;
|
encryptedPassword: string;
|
||||||
encryptedSDK: string;
|
encryptedSDK: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
availableStudentIds: number[];
|
availableStudentIds: number[];
|
||||||
};
|
};
|
||||||
|
@ -61,3 +64,21 @@ export interface ApolloContext {
|
||||||
request: FastifyRequest;
|
request: FastifyRequest;
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeInfo {
|
||||||
|
id: string;
|
||||||
|
expires: Date;
|
||||||
|
scopes: string[];
|
||||||
|
clientId: string;
|
||||||
|
studentIds: number[];
|
||||||
|
tokenSecret: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPassword: string;
|
||||||
|
encryptedSDK: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
codeChallenge?: CodeChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeContent {
|
||||||
|
tk: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
|
import base64url from 'base64url';
|
||||||
|
|
||||||
export function generatePrivatePublicPair(): Promise<{
|
export function generatePrivatePublicPair(): Promise<{
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
}> {
|
}> {
|
||||||
return util.promisify(crypto.generateKeyPair)('rsa', {
|
return util.promisify(crypto.generateKeyPair)('rsa', {
|
||||||
modulusLength: 1024,
|
modulusLength: 2048,
|
||||||
publicKeyEncoding: {
|
publicKeyEncoding: {
|
||||||
type: 'spki',
|
type: 'spki',
|
||||||
format: 'pem',
|
format: 'pem',
|
||||||
|
@ -18,30 +19,30 @@ export function generatePrivatePublicPair(): Promise<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createKey(): Buffer {
|
export function createKey(): string {
|
||||||
return crypto.randomBytes(32);
|
return base64url.encode(crypto.randomBytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptSymmetrical(value: string, key: Buffer): string {
|
export function encryptSymmetrical(value: string, key: string): string {
|
||||||
const ivBuffer = crypto.randomBytes(16);
|
const ivBuffer = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, ivBuffer);
|
const cipher = crypto.createCipheriv('aes-256-cbc', base64url.toBuffer(key), ivBuffer);
|
||||||
const encrypted = Buffer.concat([cipher.update(value), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(value), cipher.final()]);
|
||||||
return `${encrypted.toString('base64')}@${ivBuffer.toString('base64')}`;
|
return `${base64url.encode(encrypted)}@${base64url.encode(ivBuffer)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decryptSymmetrical(encrypted: string, key: Buffer): string {
|
export function decryptSymmetrical(encrypted: string, key: string): string {
|
||||||
const [iv, encryptedData] = encrypted.split('@');
|
const [encryptedData, iv] = encrypted.split('@');
|
||||||
const ivBuffer = Buffer.from(iv, 'base64');
|
const ivBuffer = base64url.toBuffer(iv);
|
||||||
const encryptedBuffer = Buffer.from(encryptedData, 'base64');
|
const encryptedBuffer = base64url.toBuffer(encryptedData);
|
||||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, ivBuffer);
|
const decipher = crypto.createDecipheriv('aes-256-cbc', base64url.toBuffer(key), ivBuffer);
|
||||||
const decrypted = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
|
const decrypted = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
|
||||||
return decrypted.toString();
|
return decrypted.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptWithPublicKey(value: string, publicKey: string): string {
|
export function encryptWithPublicKey(value: string, publicKey: string): string {
|
||||||
return crypto.publicEncrypt(publicKey, Buffer.from(value)).toString('base64');
|
return base64url.encode(crypto.publicEncrypt(publicKey, Buffer.from(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decryptWithPrivateKey(encrypted: string, privateKey: string): string {
|
export function decryptWithPrivateKey(encrypted: string, privateKey: string): string {
|
||||||
return crypto.privateDecrypt(privateKey, Buffer.from(encrypted, 'base64')).toString();
|
return crypto.privateDecrypt(privateKey, base64url.toBuffer(encrypted)).toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ export function requireEnvBase64(name: string): Buffer {
|
||||||
return Buffer.from(requireEnv(name), 'base64');
|
return Buffer.from(requireEnv(name), 'base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIntStrict(value: string, radix = 10): number {
|
export function parseIntStrict(value: string): number {
|
||||||
const number = parseInt(value, radix);
|
const number = parseInt(value, 10);
|
||||||
if (_.isNaN(number)) throw new Error(`Cannot parse ${value} to int`);
|
if (_.isNaN(number)) throw new Error(`Cannot parse ${value} to int`);
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
@ -44,13 +44,13 @@ export function validateOptionalParam(key: string, value: unknown): asserts valu
|
||||||
if (typeof value !== 'string') throw new ParamError(`${key} param should be a string`);
|
if (typeof value !== 'string') throw new ParamError(`${key} param should be a string`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseScopeParam(key: string, value: unknown): string[] {
|
export function parseArrayParam(key: string, value: unknown): string[] {
|
||||||
if (value === undefined) throw new ParamError(`${key} param is missing`);
|
if (value === undefined) throw new ParamError(`${key} param is missing`);
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
if (value === '') return [];
|
if (value === '') return [];
|
||||||
return value.split('+').map((scope) => scope.trim());
|
return value.split(/[+ ]/g).map((item) => item.trim());
|
||||||
}
|
}
|
||||||
if (_.isArray(value)) return value.flatMap((e) => parseScopeParam(key, e));
|
if (_.isArray(value)) return value.flatMap((e) => parseArrayParam(key, e));
|
||||||
throw new ParamError(`${key} param should be a string or an array of strings`);
|
throw new ParamError(`${key} param should be a string or an array of strings`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,6 @@ export async function verifyCaptchaResponse(response: string): Promise<boolean>
|
||||||
response,
|
response,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(body);
|
|
||||||
return body.success;
|
return body.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-form @submit.prevent="submit">
|
|
||||||
<v-card-title
|
<v-card-title
|
||||||
v-if="mode === StudentsMode.One"
|
v-if="mode === StudentsMode.One"
|
||||||
>
|
>
|
||||||
|
@ -59,7 +58,6 @@
|
||||||
Przydziel dostęp
|
Przydziel dostęp
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-form>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -113,7 +111,7 @@ export default class StudentsWindow extends Vue {
|
||||||
|
|
||||||
get allowUrl() {
|
get allowUrl() {
|
||||||
if (!this.valid) return null;
|
if (!this.valid) return null;
|
||||||
if (this.mode === StudentsMode.One) return `/api/website/allow?prompt_id=${this.promptInfo.id}`;
|
if (this.mode === StudentsMode.None) return `/api/website/allow?prompt_id=${this.promptInfo.id}`;
|
||||||
const studentIds = this.pickedStudents.map(encodeURIComponent).join('+');
|
const studentIds = this.pickedStudents.map(encodeURIComponent).join('+');
|
||||||
return `/api/website/allow?prompt_id=${this.promptInfo.id}&student_ids=${studentIds}`;
|
return `/api/website/allow?prompt_id=${this.promptInfo.id}&student_ids=${studentIds}`;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue