diff --git a/docs b/docs index 97b8ef607..3cc94e532 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 97b8ef607aa78cc38d04c030fabd8d55806cd1df +Subproject commit 3cc94e532274ee194ac05edb33b76980e415b3dd diff --git a/src/app/_components/OmegaId/identification/OmegaIdElement.module.scss b/src/app/_components/OmegaId/identification/OmegaIdElement.module.scss index 9f94b52d5..811707e85 100644 --- a/src/app/_components/OmegaId/identification/OmegaIdElement.module.scss +++ b/src/app/_components/OmegaId/identification/OmegaIdElement.module.scss @@ -2,8 +2,4 @@ .OmegaIdElement { max-width: 400px; - > p { - width: 100%; - text-align: center; - } -} \ No newline at end of file +} diff --git a/src/app/_components/OmegaId/identification/OmegaIdElement.tsx b/src/app/_components/OmegaId/identification/OmegaIdElement.tsx index 7ca89981c..4f3098c14 100644 --- a/src/app/_components/OmegaId/identification/OmegaIdElement.tsx +++ b/src/app/_components/OmegaId/identification/OmegaIdElement.tsx @@ -2,27 +2,20 @@ import styles from './OmegaIdElement.module.scss' import { generateOmegaIdAction } from '@/actions/omegaid/generate' import { readJWTPayload } from '@/jwt/jwtReadUnsecure' +import { compressOmegaId } from '@/services/omegaid/compress' import { useQRCode } from 'next-qrcode' import { useEffect, useState } from 'react' const EXPIRY_THRESHOLD = 60 -type PropTypes = { +export default function OmegaIdElement({ token }: { token: string, -} - -export default function OmegaIdElement({ token }: PropTypes) { +}) { const [tokenState, setTokenState] = useState(token) const { SVG } = useQRCode() - const JWTPayload = readJWTPayload<{ - gn?: string, - sn?: string, - }>(token) - - const firstname = JWTPayload.gn ?? '' - const lastname = JWTPayload.sn ?? '' + const JWTPayload = readJWTPayload(tokenState) const [expiryTime, setExpiryTime] = useState(new Date((JWTPayload.exp - EXPIRY_THRESHOLD) * 1000)) @@ -48,9 +41,7 @@ export default function OmegaIdElement({ token }: PropTypes) { return
- -

{firstname} {lastname}

} diff --git a/src/app/_components/OmegaId/reader/OmegaIdReader.tsx b/src/app/_components/OmegaId/reader/OmegaIdReader.tsx index 70e0ce1fd..14fd9998d 100644 --- a/src/app/_components/OmegaId/reader/OmegaIdReader.tsx +++ b/src/app/_components/OmegaId/reader/OmegaIdReader.tsx @@ -6,6 +6,7 @@ import { Html5QrcodeScanner } from 'html5-qrcode' import { useEffect, useState } from 'react' import { v4 as uuid } from 'uuid' import type { OmegaId } from '@/services/omegaid/Types' +import { decomporessOmegaId as decompressOmegaId } from '@/services/omegaid/compress' /** * Renders a component for reading OmegaId QR codes. @@ -51,11 +52,17 @@ export default function OmegaIdReader({ let lastReadTime = 0 let lastReadUserId = -1 - html5QrcodeScanner.render(async (token) => { - const parse = await parseJWT(token, publicKey, expiryOffset ?? 100) + html5QrcodeScanner.render(async (rawToken) => { + const token = decompressOmegaId(rawToken) + if (!token.success) { + setFeedBack({ + status: 'ERROR', + text: 'Ugyldig QR kode' + }) + return + } + const parse = await parseJWT(token.data, publicKey, expiryOffset ?? 100, 'omegaid') if (!parse.success) { - console.log(parse) - const msg = parse.error?.map(e => e.message).join(' / ') ?? 'Ukjent feil' setFeedBack({ diff --git a/src/app/users/[username]/page.module.scss b/src/app/users/[username]/page.module.scss index 69bae29a8..966cc0b7f 100644 --- a/src/app/users/[username]/page.module.scss +++ b/src/app/users/[username]/page.module.scss @@ -66,8 +66,8 @@ } .omegaId { - min-width: 50vw; - min-height: 50vw; + min-width: min(50vw, 400px); + min-height: min(50vw, 400px); display: grid; place-items: center; > * { diff --git a/src/app/users/[username]/page.tsx b/src/app/users/[username]/page.tsx index 8cddfcc63..0e93cfc03 100644 --- a/src/app/users/[username]/page.tsx +++ b/src/app/users/[username]/page.tsx @@ -47,6 +47,8 @@ export default async function User({ params }: PropTypes) { { username: profile.user.username } ).auth(session) + const showOmegaId = session.user?.username === params.username + return (
@@ -57,7 +59,7 @@ export default async function User({ params }: PropTypes) {

{`${profile.user.firstname} ${profile.user.lastname}`}

- @@ -67,7 +69,7 @@ export default async function User({ params }: PropTypes) {
-
+ }
{ studyProgramme && ( diff --git a/src/jwt/jwt.ts b/src/jwt/jwt.ts index beee35138..d9eec9036 100644 --- a/src/jwt/jwt.ts +++ b/src/jwt/jwt.ts @@ -29,7 +29,7 @@ export function generateJWT( throw new ServerError('INVALID CONFIGURATION', 'Missing secret for JWT generation') } - return sign(payload, asymetric ? process.env.JWT_PRIVATE_KEY : process.env.NEXTAUTH_SECRET, { + return sign(payload, Buffer.from(asymetric ? process.env.JWT_PRIVATE_KEY : process.env.NEXTAUTH_SECRET), { audience: aud, algorithm: asymetric ? 'ES256' : 'HS256', issuer: JWT_ISSUER, diff --git a/src/services/omegaid/ConfigVars.ts b/src/services/omegaid/ConfigVars.ts index 712955b03..dfad0f692 100644 --- a/src/services/omegaid/ConfigVars.ts +++ b/src/services/omegaid/ConfigVars.ts @@ -1,11 +1,2 @@ -import type { UserFiltered } from '@/services/users/Types' - - -export const omegaIdFields = [ - 'id', - 'firstname', - 'lastname', - 'username', -] as const satisfies (keyof UserFiltered)[] export const OmegaIdExpiryTime = 60 * 5 // 5 minutes diff --git a/src/services/omegaid/Types.ts b/src/services/omegaid/Types.ts index 981fe9f8b..3d82cbe70 100644 --- a/src/services/omegaid/Types.ts +++ b/src/services/omegaid/Types.ts @@ -1,11 +1,9 @@ -import type { omegaIdFields } from './ConfigVars' import type { UserFiltered } from '@/services/users/Types' -export type OmegaId = Pick +export type OmegaId = Pick export type OmegaIdJWT = { + iat: number, + exp: number, sub: UserFiltered['id'], - usrnm: UserFiltered['username'], - gn: UserFiltered['firstname'], - sn: UserFiltered['lastname'], } diff --git a/src/services/omegaid/compress.ts b/src/services/omegaid/compress.ts new file mode 100644 index 000000000..6f32b56fd --- /dev/null +++ b/src/services/omegaid/compress.ts @@ -0,0 +1,110 @@ +import { JWT_ISSUER } from '@/auth/ConfigVars' +import type { ActionReturn, ActionReturnError } from '@/actions/Types' +import type { OmegaIdJWT } from '@/services/omegaid/Types' + +/** + * This file handles compression and decompression of omegaID + */ +export function compressOmegaId(token: string): string { + const parts = token.split('.') + const payloadCompressed = compressPayload(parts[1]) + const payload = base64ToBigInt(payloadCompressed) + const sign = base64ToBigInt(parts[2]) + const ret = `${payload}.${sign}` + return ret +} + +function decodeBase64Url(base64: string) { + return atob(base64.replaceAll('-', '+').replaceAll('_', '/')) +} + +function encodeBase64Url(data: string): string { + return btoa(data).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '') +} + +function base64ToBigInt(base64: string): string { + const binaryString = decodeBase64Url(base64) + let bigint = BigInt(0) + + // Convert binary string to BigInt + for (let i = 0; i < binaryString.length; i++) { + bigint = (bigint << BigInt(8)) | BigInt(binaryString.charCodeAt(i)) + } + + return bigint.toString() +} + +function bigIntToBase64(bigIntString: string): string { + let bigInt = BigInt(bigIntString) + let binaryString = '' + + while (bigInt > BigInt(0)) { + const nextChar = bigInt & BigInt(0xFF) + binaryString += String.fromCharCode(Number(nextChar)) + bigInt >>= BigInt(8) + } + + binaryString = binaryString.split('').reverse().join('') + + return encodeBase64Url(binaryString) +} + +function compressPayload(payload: string): string { + const binaryString = decodeBase64Url(payload) + const payloadString = binaryString.toString() + const payloadJSON = JSON.parse(payloadString) as OmegaIdJWT + const shortPayloadString = `${payloadJSON.sub},${payloadJSON.iat},${payloadJSON.exp}` + return encodeBase64Url(shortPayloadString) +} + +function decompressPayload(rawdata: string): string { + const base64String = bigIntToBase64(rawdata) + const byteString = decodeBase64Url(base64String) + const dataString = byteString.toString().split(',') + const payload = { + sub: Number(dataString[0]), + iat: Number(dataString[1]), + exp: Number(dataString[2]), + aud: 'omegaid', + iss: JWT_ISSUER, + } + const payloadString = JSON.stringify(payload) + + return encodeBase64Url(payloadString) +} + +export function decomporessOmegaId(rawdata: string): ActionReturn { + const header = { + alg: 'ES256', + typ: 'JWT' + } + const headerJSONString = JSON.stringify(header) + const headerB64String = encodeBase64Url(headerJSONString) + + const errorReturn: ActionReturnError = { + success: false, + errorCode: 'JWT INVALID', + httpCode: 400, + error: [{ + message: 'QR code is not an OmegaId', + }], + } + + const rawDataSplit = rawdata.split('.') + if (rawDataSplit.length !== 2) { + return errorReturn + } + + try { + const payload = decompressPayload(rawDataSplit[0]) + + const signature = bigIntToBase64(rawDataSplit[1]) + + return { + success: true, + data: `${headerB64String}.${payload}.${signature}`, + } + } catch { + return errorReturn + } +} diff --git a/src/services/omegaid/generate.ts b/src/services/omegaid/generate.ts index b05f3b141..6a6377016 100644 --- a/src/services/omegaid/generate.ts +++ b/src/services/omegaid/generate.ts @@ -1,15 +1,12 @@ import 'server-only' import { OmegaIdExpiryTime } from './ConfigVars' import { generateJWT } from '@/jwt/jwt' -import type { OmegaId, OmegaIdJWT } from './Types' +import type { OmegaId } from './Types' export function generateOmegaId(user: OmegaId): string { - const payload: OmegaIdJWT = { + const payload = { sub: user.id, - usrnm: user.username, - gn: user.firstname, - sn: user.lastname, } return generateJWT('omegaid', payload, OmegaIdExpiryTime, true)