Skip to content

Commit

Permalink
feat: Add compression to omegaID
Browse files Browse the repository at this point in the history
  • Loading branch information
theodorklauritzen committed Nov 7, 2024
1 parent dc2bed8 commit aa1822c
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 48 deletions.
2 changes: 1 addition & 1 deletion docs
Submodule docs updated from 97b8ef to 3cc94e
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@

.OmegaIdElement {
max-width: 400px;
> p {
width: 100%;
text-align: center;
}
}
}
19 changes: 5 additions & 14 deletions src/app/_components/OmegaId/identification/OmegaIdElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -48,9 +41,7 @@ export default function OmegaIdElement({ token }: PropTypes) {

return <div className={styles.OmegaIdElement}>
<SVG
text={tokenState}
text={compressOmegaId(tokenState)}
/>

<p>{firstname} {lastname}</p>
</div>
}
15 changes: 11 additions & 4 deletions src/app/_components/OmegaId/reader/OmegaIdReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions src/app/users/[username]/page.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
> * {
Expand Down
6 changes: 4 additions & 2 deletions src/app/users/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.wrapper}>
<div className={styles.profile}>
Expand All @@ -57,7 +59,7 @@ export default async function User({ params }: PropTypes) {
<div className={styles.header}>
<div className={styles.nameAndId}>
<h1>{`${profile.user.firstname} ${profile.user.lastname}`}</h1>
<PopUp
{showOmegaId && <PopUp
showButtonClass={styles.omegaIdOpen}
showButtonContent={
<FontAwesomeIcon icon={faQrcode} />
Expand All @@ -67,7 +69,7 @@ export default async function User({ params }: PropTypes) {
<div className={styles.omegaId}>
<OmegaId />
</div>
</PopUp>
</PopUp> }
</div>
{
studyProgramme && (
Expand Down
2 changes: 1 addition & 1 deletion src/jwt/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function generateJWT<T extends object>(
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,
Expand Down
9 changes: 0 additions & 9 deletions src/services/omegaid/ConfigVars.ts
Original file line number Diff line number Diff line change
@@ -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
8 changes: 3 additions & 5 deletions src/services/omegaid/Types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { omegaIdFields } from './ConfigVars'
import type { UserFiltered } from '@/services/users/Types'

export type OmegaId = Pick<UserFiltered, typeof omegaIdFields[number]>
export type OmegaId = Pick<UserFiltered, 'id'>

export type OmegaIdJWT = {
iat: number,
exp: number,
sub: UserFiltered['id'],
usrnm: UserFiltered['username'],
gn: UserFiltered['firstname'],
sn: UserFiltered['lastname'],
}
110 changes: 110 additions & 0 deletions src/services/omegaid/compress.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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
}
}
7 changes: 2 additions & 5 deletions src/services/omegaid/generate.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down

0 comments on commit aa1822c

Please sign in to comment.