diff --git a/api/package-lock.json b/api/package-lock.json index 2f356bbf..bea0b326 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -30,6 +30,7 @@ "pino-pretty": "^10.0.0", "vite-node": "^0.29.2", "vitest": "^0.29.2", + "wildcard-match": "^5.1.2", "zod": "^3.21.4" }, "devDependencies": { @@ -6815,6 +6816,11 @@ "node": ">=8" } }, + "node_modules/wildcard-match": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.2.tgz", + "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==" + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -11737,6 +11743,11 @@ } } }, + "wildcard-match": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.2.tgz", + "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/api/package.json b/api/package.json index aba276e9..ce68097b 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,7 @@ "pino-pretty": "^10.0.0", "vite-node": "^0.29.2", "vitest": "^0.29.2", + "wildcard-match": "^5.1.2", "zod": "^3.21.4" }, "devDependencies": { diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 3aa8e899..49100fc5 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -36,6 +36,7 @@ model Session { revokedAt DateTime? referenceTokenId String isUserCreated Boolean @default(false) + allowedOrigins String // comma separated strings creator User @relation(fields: [createdBy], references: [id], onDelete: Cascade) diff --git a/api/src/generated/nexus.ts b/api/src/generated/nexus.ts index f023c6d3..0e1cc6ad 100644 --- a/api/src/generated/nexus.ts +++ b/api/src/generated/nexus.ts @@ -41,6 +41,7 @@ declare global { export interface NexusGenInputs { SessionCreate: { // input type + allowedOrigins: string; // String! expiryDurationSeconds?: number | null; // Int name: string; // String! } @@ -80,6 +81,7 @@ export interface NexusGenObjects { Mutation: {}; Query: {}; Session: { // root type + allowedOrigins?: string | null; // String createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase! createdBy: string; // String! id: string; // String! @@ -138,6 +140,7 @@ export interface NexusGenFieldTypes { sessions: Array | null; // [Session] } Session: { // field return type + allowedOrigins: string | null; // String createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase! createdBy: string; // String! id: string; // String! @@ -186,6 +189,7 @@ export interface NexusGenFieldTypeNames { sessions: 'Session' } Session: { // field return type name + allowedOrigins: 'String' createdAt: 'GQLDateBase' createdBy: 'String' id: 'String' diff --git a/api/src/generated/schema.graphql b/api/src/generated/schema.graphql index 5646be89..ba3fb820 100644 --- a/api/src/generated/schema.graphql +++ b/api/src/generated/schema.graphql @@ -36,6 +36,7 @@ type Query { } type Session { + allowedOrigins: String createdAt: GQLDateBase! createdBy: String! id: String! @@ -47,6 +48,7 @@ type Session { } input SessionCreate { + allowedOrigins: String! expiryDurationSeconds: Int name: String! } diff --git a/api/src/graphql/context.ts b/api/src/graphql/context.ts index 81f03ab8..43738963 100644 --- a/api/src/graphql/context.ts +++ b/api/src/graphql/context.ts @@ -15,6 +15,7 @@ export interface Context { prisma: typeof prisma; getSession: () => Promise; apolloLogger: pino.Logger; + origin: string | undefined; } type CreateContextParams = { @@ -29,11 +30,13 @@ export function createContext(params: CreateContextParams): Context { const authorizationHeader = req.get('Authorization'); const cookieAuthHeader = req.cookies['gql:default']; const token = authorizationHeader?.replace('Bearer ', ''); + const origin = req.get('Origin'); return { request: params, prisma, apolloLogger, - getSession: async () => prisma.session.getSessionByToken(token || cookieAuthHeader), + getSession: async () => prisma.session.getSessionByToken(origin, token || cookieAuthHeader), + origin, }; } diff --git a/api/src/modules/Session/helpers.ts b/api/src/modules/Session/helpers.ts new file mode 100644 index 00000000..d20891b0 --- /dev/null +++ b/api/src/modules/Session/helpers.ts @@ -0,0 +1,81 @@ +import type { PrismaClient, Prisma } from '@prisma/client'; +import { randomUUID } from 'crypto'; +import { GraphQLError } from 'graphql'; +import wildcard from 'wildcard-match'; +import { token as tokenUtils } from '../../helpers'; + +function parseOriginMarkup(originParam: string): string { + if (originParam === '*') { + return '*'; + } + const trimmedOriginParam = originParam.trim(); + const origins = trimmedOriginParam.split(',').map((origin) => origin.trim()); + origins.forEach((origin) => { + if (!origin.startsWith('http://') && !origin.startsWith('https://')) { + throw new GraphQLError("Origin must start with 'http://' or 'https://'", { + extensions: { code: 'INVALID_ORIGIN_PROTOCOL' }, + }); + } + }); + return origins.join(','); +} + +export function validateOriginAgainstAllowed( + allowedOrigins: string, + originReceived?: string, +) { + if (allowedOrigins === '*') { + return; + } + if (!originReceived) { + throw new GraphQLError('Origin not provided', { + extensions: { code: 'ORIGIN_HEADER_MISSING' }, + }); + } + const allowedOriginsSplit = allowedOrigins.split(','); + if (!wildcard(allowedOriginsSplit)(originReceived)) { + throw new GraphQLError('Access denied due to origin restriction', { + extensions: { code: 'ORIGIN_FORBIDDEN' }, + }); + } +} + +async function newSession( + prisma: PrismaClient, + session: Prisma.SessionCreateInput, +) { + return prisma.session.create({ + data: session, + }); +} + +export const generateTokenAndSession = async ( + prisma: PrismaClient, + userId: string, + session: { expiryDurationSeconds?: number | null; name: string; allowedOrigins: string }, + isUserCreated: boolean = false, +) => { + const createId = randomUUID(); + const createdToken = tokenUtils.generate(createId, session.expiryDurationSeconds); + const expiryDate = tokenUtils.getExpiryDateFromToken(createdToken); + const formattedToken = tokenUtils.format(createdToken); + const parsedAllowedOrigins = parseOriginMarkup(session.allowedOrigins); + const createData = { + allowedOrigins: parsedAllowedOrigins, + name: session.name, + referenceExpiryDate: expiryDate, + id: createId, + referenceTokenId: formattedToken, + isUserCreated, + creator: { + connect: { + id: userId, + }, + }, + }; + const createdSession = await newSession(prisma, createData); + return { + token: createdToken, + session: createdSession, + }; +}; diff --git a/api/src/modules/Session/model.ts b/api/src/modules/Session/model.ts index ff8f362d..fe74cf70 100644 --- a/api/src/modules/Session/model.ts +++ b/api/src/modules/Session/model.ts @@ -1,10 +1,10 @@ -import type { PrismaClient, Prisma } from '@prisma/client'; +import type { PrismaClient } from '@prisma/client'; import { inputObjectType, objectType } from 'nexus/dist'; -import { randomUUID } from 'crypto'; import { GraphQLError } from 'graphql'; import ms from 'ms'; import { token as tokenUtils } from '../../helpers'; import { JWT_EXPIRATION_PERIOD } from '../../env'; +import { validateOriginAgainstAllowed, generateTokenAndSession } from './helpers'; export const Session = objectType({ name: 'Session', @@ -17,6 +17,7 @@ export const Session = objectType({ t.nonNull.boolean('isUserCreated'); t.string('name'); t.date('revokedAt'); + t.string('allowedOrigins'); }, }); @@ -25,6 +26,7 @@ export const SessionCreate = inputObjectType({ definition(t) { t.int('expiryDurationSeconds'); t.nonNull.string('name'); + t.nonNull.string('allowedOrigins'); }, }); @@ -36,44 +38,6 @@ export const SessionCreateOutput = objectType({ }, }); -async function newSession( - prisma: PrismaClient, - session: Prisma.SessionCreateInput, -) { - return prisma.session.create({ - data: session, - }); -} - -const generateTokenAndSession = async ( - prisma: PrismaClient, - userId: string, - session: { expiryDurationSeconds?: number | null; name: string }, - isUserCreated: boolean = false, -) => { - const createId = randomUUID(); - const createdToken = tokenUtils.generate(createId, session.expiryDurationSeconds); - const expiryDate = tokenUtils.getExpiryDateFromToken(createdToken); - const formattedToken = tokenUtils.format(createdToken); - const createData = { - name: session.name, - referenceExpiryDate: expiryDate, - id: createId, - referenceTokenId: formattedToken, - isUserCreated, - creator: { - connect: { - id: userId, - }, - }, - }; - const createdSession = await newSession(prisma, createData); - return { - token: createdToken, - session: createdSession, - }; -}; - export function getSessionCrud(prisma: PrismaClient) { return { listSessions: async (userId: string) => prisma.session.findMany({ @@ -112,22 +76,23 @@ export function getSessionCrud(prisma: PrismaClient) { throw new GraphQLError('Failed to revoke session', { extensions: { code: 'REVOKE_SESSION_FAILED' } }); } }, - createSignInSession: async (userId: string) => generateTokenAndSession( + createSignInSession: async (userId: string, origin: string = '*') => generateTokenAndSession( prisma, userId, - { expiryDurationSeconds: ms(JWT_EXPIRATION_PERIOD) / 1000, name: 'Sign in' }, + { expiryDurationSeconds: ms(JWT_EXPIRATION_PERIOD) / 1000, name: 'Sign in', allowedOrigins: origin }, ), - createSignUpSession: async (userId: string) => generateTokenAndSession( + createSignUpSession: async (userId: string, origin: string = '*') => generateTokenAndSession( prisma, userId, - { expiryDurationSeconds: ms(JWT_EXPIRATION_PERIOD) / 1000, name: 'Sign up' }, + { expiryDurationSeconds: ms(JWT_EXPIRATION_PERIOD) / 1000, name: 'Sign up', allowedOrigins: origin }, ), createCustomSession: async ( userId: string, - session: { expiryDurationSeconds?: number | null; name: string }, + session: { expiryDurationSeconds?: number | null; name: string, allowedOrigins: string }, isUserCreated: boolean = false, ) => generateTokenAndSession(prisma, userId, session, isUserCreated), async getSessionByToken( + origin?: string, token?: string, ) { if (!token) { @@ -150,6 +115,7 @@ export function getSessionCrud(prisma: PrismaClient) { extensions: { code: 'SESSION_EXPIRED' }, }); } + validateOriginAgainstAllowed(session.allowedOrigins, origin); return session; }, diff --git a/api/src/modules/User/resolvers.ts b/api/src/modules/User/resolvers.ts index ef1d72ef..2098dec2 100644 --- a/api/src/modules/User/resolvers.ts +++ b/api/src/modules/User/resolvers.ts @@ -19,7 +19,7 @@ export const signIn = mutationField('signIn', { }, resolve: async (_parent, { user: userNamePass }, ctx) => { const { id } = await ctx.prisma.user.getUserByUsernamePassword(userNamePass); - return ctx.prisma.session.createSignInSession(id); + return ctx.prisma.session.createSignInSession(id, ctx.origin); }, }); @@ -30,6 +30,6 @@ export const signUp = mutationField('signUp', { }, resolve: async (_parent, { user }, ctx) => { const { id } = await ctx.prisma.user.createUser(user); - return ctx.prisma.session.createSignUpSession(id); + return ctx.prisma.session.createSignUpSession(id, ctx.origin); }, }); diff --git a/api/tests/auth.test.ts b/api/tests/auth.test.ts index edb7cde2..c8f714e1 100644 --- a/api/tests/auth.test.ts +++ b/api/tests/auth.test.ts @@ -38,6 +38,7 @@ test('Authentication: sign up, sign in, request protected enpoint', async () => const token = signInResponse?.signIn?.token; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const meResponse = (await executeGraphQlQuery(meQuery)) as Record< string, diff --git a/api/tests/session.test.ts b/api/tests/session.test.ts index cd5f8b26..ddfdd1e1 100644 --- a/api/tests/session.test.ts +++ b/api/tests/session.test.ts @@ -25,6 +25,7 @@ const getRevokeSessionMutation = (sessionId: string) => builder.mutation({ const getCreateSessionMutation = ( name: string, + allowedOrigins: string, expiryDurationSeconds?: number | null, ) => builder.mutation({ operation: 'createSession', @@ -33,6 +34,7 @@ const getCreateSessionMutation = ( value: { name, expiryDurationSeconds, + allowedOrigins, }, type: 'SessionCreate', required: true, @@ -44,6 +46,7 @@ const getCreateSessionMutation = ( test('Auth session: list', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const sessionsResponse = (await executeGraphQlQuery(listSessionsQuery)) as any; const executedAt = new Date(); expect(sessionsResponse?.sessions?.length).toBe(1); @@ -60,6 +63,7 @@ test('Auth session: list, no auth', async () => { test('Auth session: revoke', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const sessionsResponse = (await executeGraphQlQuery(listSessionsQuery)) as any; const session = sessionsResponse?.sessions[0]; const mutation = getRevokeSessionMutation(session.id); @@ -78,6 +82,7 @@ test('Auth session: revoke, no auth', async () => { test('Auth session: revoke unexistant', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const mutation = getRevokeSessionMutation('asdf'); const revokeResponse = (await executeGraphQlQuery(mutation)) as any; expect(revokeResponse?.errors[0].message).toBe('Session not found'); @@ -87,9 +92,10 @@ test('Auth session: revoke unexistant', async () => { test('Auth session: create expirable', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const name = 'JoJo'; const expectedExpiryDate = new Date(); - const mutation = getCreateSessionMutation(name, 1); + const mutation = getCreateSessionMutation(name, '*', 1); const createResponse = (await executeGraphQlQuery(mutation)) as any; expect(createResponse?.createSession?.session.name).toBe(name); expect( @@ -107,6 +113,7 @@ test('Auth session: create expirable', async () => { ); expect(userCreatedList.some((i) => i)).toBe(true); ctx.client.setHeader('Authorization', `Bearer ${createdToken}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); // eslint-disable-next-line no-promise-executor-return await new Promise((resolve) => setTimeout(resolve, 20)); const meResponse = (await executeGraphQlQuery(meQuery)) as any; @@ -117,8 +124,9 @@ test('Auth session: create expirable', async () => { test('Auth session: create unexpirable', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const name = 'JoJo'; - const mutation = getCreateSessionMutation(name, null); + const mutation = getCreateSessionMutation(name, '*', null); const createResponse = (await executeGraphQlQuery(mutation)) as any; expect(createResponse?.createSession?.session.name).toBe(name); expect( @@ -132,6 +140,7 @@ test('Auth session: create unexpirable', async () => { expect(userCreatedList.some((i) => i)).toBe(true); const customToken = createResponse?.createSession?.token; ctx.client.setHeader('Authorization', `Bearer ${customToken}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const meResponse = (await executeGraphQlQuery(meQuery)) as any; expect(meResponse?.me?.username).toBe(USERNAME); }); @@ -139,20 +148,22 @@ test('Auth session: create unexpirable', async () => { test('Auth session: revoked session is forbidden', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const name = 'JoJo'; - let mutation = getCreateSessionMutation(name, 3600); + let mutation = getCreateSessionMutation(name, '*', 3600); const createResponse = (await executeGraphQlQuery(mutation)) as any; const sessionId = createResponse?.createSession?.session.id; mutation = getRevokeSessionMutation(sessionId); await executeGraphQlQuery(mutation); ctx.client.setHeader('Authorization', `Bearer ${createResponse.createSession.token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const sessionsResponse = (await executeGraphQlQuery(listSessionsQuery)) as any; expect(sessionsResponse.errors[0].message).toBe('Session expired'); }); test('Auth session: cant revoke without auth', async () => { const name = 'JoJo'; - const mutation = getCreateSessionMutation(name, 3600); + const mutation = getCreateSessionMutation(name, '*', 3600); const createResponse = (await executeGraphQlQuery(mutation)) as any; expect(createResponse.errors[0].message).toBe('Not authenticated'); }); @@ -160,8 +171,9 @@ test('Auth session: cant revoke without auth', async () => { test('Auth session: cant revoke twice', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const name = 'JoJo'; - let mutation = getCreateSessionMutation(name, 3600); + let mutation = getCreateSessionMutation(name, '*', 3600); const createResponse = (await executeGraphQlQuery(mutation)) as any; const sessionId = createResponse?.createSession?.session.id; mutation = getRevokeSessionMutation(sessionId); @@ -173,11 +185,80 @@ test('Auth session: cant revoke twice', async () => { test('Auth session: revoke sessions of other users', async () => { const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const sessionsResponse = (await executeGraphQlQuery(listSessionsQuery)) as any; const session = sessionsResponse?.sessions[0]; const { token: secondUserToken } = (await executeGraphQlQuery(getSignUpMutation('scott', 'malcinson')) as any).signUp; ctx.client.setHeader('Authorization', `Bearer ${secondUserToken}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); const mutation = getRevokeSessionMutation(session.id); const revokeResponse = (await executeGraphQlQuery(mutation)) as any; expect(revokeResponse.errors[0].message).toBe('Failed to revoke session'); }); + +test('Auth session: origin restriction wrong origin', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', 'http://google.com', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + const sessionToken = sessionResponse.createSession?.token; + ctx.client.setHeader('Authorization', `Bearer ${sessionToken}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const meResponse = (await executeGraphQlQuery(meQuery)) as any; + expect(meResponse.errors[0].message).toBe('Access denied due to origin restriction'); +}); + +test('Auth session: origin restriction success', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', 'http://google.com', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + const sessionToken = sessionResponse.createSession?.token; + ctx.client.setHeader('Authorization', `Bearer ${sessionToken}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + ctx.client.setHeader('Origin', 'http://google.com'); + const meResponse = (await executeGraphQlQuery(meQuery)) as any; + expect(meResponse.me.username).toBe(USERNAME); +}); + +test('Auth session: origin restriction missing header', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', 'http://google.com', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + const sessionToken = sessionResponse.createSession?.token; + ctx.client.setHeader('Authorization', `Bearer ${sessionToken}`); + ctx.client.setHeader('Origin', ''); + const meResponse = (await executeGraphQlQuery(meQuery)) as any; + expect(meResponse.errors[0].message).toBe('Origin not provided'); +}); + +test('Auth session: origin valid - contains space', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', 'http://google.com ', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + expect(sessionResponse.createSession.session.name).toBe('Origin'); +}); + +test('Auth session: origin invalid - bad protocol', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', 'htt://google.com', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + expect(sessionResponse.errors[0].message).toBe("Origin must start with 'http://' or 'https://'"); +}); + +test('Auth session: origin invalid - empty string', async () => { + const { token } = (await executeGraphQlQuery(getSignUpMutation()) as any).signUp; + ctx.client.setHeader('Authorization', `Bearer ${token}`); + ctx.client.setHeader('Origin', 'http://localhost:3000'); + const mutation = getCreateSessionMutation('Origin', '', 3600); + const sessionResponse = await executeGraphQlQuery(mutation) as any; + expect(sessionResponse.errors[0].message).toBe("Origin must start with 'http://' or 'https://'"); +}); diff --git a/frontend/components/auth/SessionForm.vue b/frontend/components/auth/SessionForm.vue index d8102e8b..aa434c63 100644 --- a/frontend/components/auth/SessionForm.vue +++ b/frontend/components/auth/SessionForm.vue @@ -6,6 +6,9 @@ + !name.value || expiryDurationSeconds.value === null) +const isCreationDisabed = computed(() => !name.value || expiryDurationSeconds.value === null || allowedOrigins.value === null) const create = async () => { isCreating.value = true try { - const token = await props.createSession(name.value, expiryDurationSeconds.value ?? null) + const token = await props.createSession(name.value, expiryDurationSeconds.value ?? null, allowedOrigins.value) name.value = '' expiryDurationSeconds.value = null createdToken.value = token diff --git a/frontend/components/auth/SessionTable.vue b/frontend/components/auth/SessionTable.vue index 227f63ec..a6623568 100644 --- a/frontend/components/auth/SessionTable.vue +++ b/frontend/components/auth/SessionTable.vue @@ -32,6 +32,10 @@ const formatDate = (dateString?: string): string => { return format(new Date(dateString), 'dd.MM.yyyy HH:mm') } +const formatCommaList = (list: string): string[] => { + return list.replace(',', ', '); +} + const revoke = async (sessionId: string) => { try { const referenceTokenId = await props.revokeSession(sessionId) @@ -67,6 +71,13 @@ const columns = [ return 'never' } }, + { + title: 'Allowed origins', + key: 'allowedOrigins', + render (session: Session) { + return formatCommaList(session.allowedOrigins) + } + }, { title: 'Status', key: 'status', diff --git a/frontend/composables/useAuth.ts b/frontend/composables/useAuth.ts index dbe06c44..a96ac577 100644 --- a/frontend/composables/useAuth.ts +++ b/frontend/composables/useAuth.ts @@ -53,8 +53,8 @@ const useAuth = function () { authStorage.value.token = data.value?.signUp?.token await checkAuthValidity() } - const createSession = async (name: string, expiryDurationSeconds: number | null) => { - const { data, error } = await useAsyncGql('createSession', { name, expiryDurationSeconds }) + const createSession = async (name: string, expiryDurationSeconds: number | null, allowedOrigins: string) => { + const { data, error } = await useAsyncGql('createSession', { name, expiryDurationSeconds, allowedOrigins }) if (error.value || !data.value?.createSession?.token) { throw new Error(error.value?.gqlErrors?.[0]?.message ?? 'Unknown error') } diff --git a/frontend/containers/SessionContainer.vue b/frontend/containers/SessionContainer.vue index 05408535..8ca5c11c 100644 --- a/frontend/containers/SessionContainer.vue +++ b/frontend/containers/SessionContainer.vue @@ -18,6 +18,7 @@ export interface Session { isUserCreated: boolean; name?: string | null | undefined; revokedAt?: any; + allowedOrigins: string; } const { createSession, revokeSession } = useAuth() @@ -44,8 +45,8 @@ const revoke = async (sessionId: string) => { return referenceTokenId } -const create = async (name: string, expiryDurationSeconds: number | null) => { - const token = await createSession(name, expiryDurationSeconds) +const create = async (name: string, expiryDurationSeconds: number | null, allowedOrigins: string) => { + const token = await createSession(name, expiryDurationSeconds, allowedOrigins) await getSessions() return token } diff --git a/frontend/graphql/createSession.gql b/frontend/graphql/createSession.gql index 890fba35..64bcccad 100644 --- a/frontend/graphql/createSession.gql +++ b/frontend/graphql/createSession.gql @@ -1,9 +1,10 @@ -mutation createSession($name: String!, $expiryDurationSeconds: Int) { - createSession(session: { name: $name, expiryDurationSeconds: $expiryDurationSeconds }) { +mutation createSession($name: String!, $expiryDurationSeconds: Int, $allowedOrigins: String!) { + createSession(session: { name: $name, expiryDurationSeconds: $expiryDurationSeconds, allowedOrigins: $allowedOrigins }) { token session { id referenceExpiryDate + allowedOrigins } } } diff --git a/frontend/graphql/sessions.gql b/frontend/graphql/sessions.gql index e53dc46a..9bbef1f8 100644 --- a/frontend/graphql/sessions.gql +++ b/frontend/graphql/sessions.gql @@ -8,5 +8,6 @@ query getSessions { isUserCreated name revokedAt + allowedOrigins } }