Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save email verification token to person's pod #5

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ export const jwt = {
export const emailDiscoveryType =
process.env.EMAIL_DISCOVERY_TYPE ??
'http://w3id.org/hospex/ns#PersonalHospexDocument'

export const verificationTokenPredicate =
'https://example.com/emailVerificationToken'
49 changes: 46 additions & 3 deletions src/controllers/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Middleware } from 'koa'
import { pick } from 'lodash'
import * as config from '../config'
import { sendMail } from '../services/mailerService'
import { findWritableSettings, getBotFetch } from '../utils'

export const initializeIntegration: Middleware = async ctx => {
// we should receive info about webId and email address
Expand Down Expand Up @@ -84,7 +85,49 @@ export const finishIntegration: Middleware = async ctx => {
pem,
{ algorithm: 'ES256' },
)
ctx.response.body = jwt
ctx.set('content-type', 'text/plain')
ctx.response.status = 200

// save the token to person
const savedTokensCount = await saveTokenToPerson(jwt, webId)

if (savedTokensCount === 0) {
ctx.response.status = 400
ctx.set('content-type', 'application/json')
ctx.response.body({
error:
"We could't find any writeable location on your Pod to save the email verifiation token. You can write it manually.",
token: jwt,
})
} else {
ctx.response.body = jwt
ctx.set('content-type', 'text/plain')
ctx.response.status = 200
}
}

/**
* Find writable settings files on person's pod
* and save the email verification token there
*/
const saveTokenToPerson = async (token: string, webId: string) => {
// find candidates for saving the token
const settings = await findWritableSettings(webId)
const authBotFetch = await getBotFetch()
// save the token to the candidates and keep track of how many succeeded
let okCount = 0
for (const uri of settings) {
const patch = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
_:patch a solid:InsertDeletePatch;
solid:inserts {
<${webId}> <${config.verificationTokenPredicate}> "${token}" .
} .`
const response = await authBotFetch(uri, {
method: 'PATCH',
headers: { 'content-type': 'text/n3' },
body: patch,
})

if (response.ok) okCount++
}

return okCount
}
119 changes: 1 addition & 118 deletions src/controllers/status.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { RdfQuery } from '@ldhop/core/dist/src'
import { QueryAndStore } from '@ldhop/core/dist/src/QueryAndStore'
import { fetchRdfDocument } from '@ldhop/core/dist/src/utils/helpers'
import { getAuthenticatedFetch as getAuthenticatedFetch6x } from 'css-authn/dist/6.x'
import { getAuthenticatedFetch as getAuthenticatedFetch7x } from 'css-authn/dist/7.x'
import { readFile } from 'fs-extra'
import { verify } from 'jsonwebtoken'
import type { DefaultContext, DefaultState, Middleware } from 'koa'
import { NamedNode, Quad } from 'n3'
import { dct, rdfs, solid, space } from 'rdf-namespaces'
import * as config from '../config'
import { findEmailVerificationTokens } from '../utils'

export const getVerifiedEmails = async (webId: string) => {
const tokens = await findEmailVerificationTokens(webId)
Expand Down Expand Up @@ -46,114 +40,3 @@ export const getStatus: Middleware<
ctx.response.body = { emailVerified: verifiedEmails.length > 0 }
ctx.response.status = 200
}

/**
* To find verified email of a person
*
* - Go to person's webId
* - Find public type index `(webId) - solid:publicTypeIndex -> (publicTypeIndex)``
* - Find instances of specific class defined in config (EMAIL_DISCOVERY_TYPE defaults to hospex:PersonalHospexDocument)
* - Find settings in the relevant instance (webId) - space:preferencesFile -> (settings)
* - In the settings, find (webId) - example:emailVerificationToken -> (JWT)
*/
const findEmailQuery: RdfQuery = [
// Go to person's webId and fetch extended profile documents, too
{
type: 'match',
subject: '?person',
predicate: rdfs.seeAlso,
pick: 'object',
target: '?extendedDocument',
},
{ type: 'add resources', variable: '?extendedDocument' },
// Find public type index
{
type: 'match',
subject: '?person',
predicate: solid.publicTypeIndex,
pick: 'object',
target: '?publicTypeIndex',
},
// Find instances of specific class defined in config (EMAIL_DISCOVERY_TYPE)
{
type: 'match',
subject: '?publicTypeIndex',
predicate: dct.references,
pick: 'object',
target: '?typeRegistration',
},
{
type: 'match',
subject: '?typeRegistration',
predicate: solid.forClass,
object: config.emailDiscoveryType,
pick: 'subject',
target: '?typeRegistrationForClass',
},
{
type: 'match',
subject: '?typeRegistrationForClass',
predicate: solid.instance,
pick: 'object',
target: `?classDocument`,
},
{ type: 'add resources', variable: '?classDocument' },
// Find settings
{
type: 'match',
subject: '?person',
predicate: space.preferencesFile,
pick: 'object',
target: '?settings',
},
{ type: 'add resources', variable: '?settings' },
]

const findEmailVerificationTokens = async (webId: string) => {
// initialize knowledge graph and follow your nose through it
// according to the query
const qas = new QueryAndStore(findEmailQuery, { person: new Set([webId]) })
await run(qas)

// Find email verification tokens
const objects = qas.store.getObjects(
new NamedNode(webId),
new NamedNode('https://example.com/emailVerificationToken'),
null,
)

return objects.map(o => o.value)
}

const fetchRdf = async (uri: string) => {
const getAuthenticatedFetch =
config.mailerCredentials.cssVersion === 6
? getAuthenticatedFetch6x
: getAuthenticatedFetch7x
const authBotFetch = await getAuthenticatedFetch(config.mailerCredentials)

const { data: quads } = await fetchRdfDocument(uri, authBotFetch)

return quads
}

/**
* Follow your nose through the linked data graph by query
*/
const run = async (qas: QueryAndStore) => {
let missingResources = qas.getMissingResources()

while (missingResources.length > 0) {
let quads: Quad[] = []
const res = missingResources[0]
try {
quads = await fetchRdf(missingResources[0])
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
} finally {
qas.addResource(res, quads)
missingResources = qas.getMissingResources()
}
}
}
13 changes: 13 additions & 0 deletions src/test/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { createSandbox } from 'sinon'
import { v4 as uuidv4 } from 'uuid'
import * as config from '../../config'
import * as mailerService from '../../services/mailerService'
import { setupEmailSettings } from './setupPod'
import { Person } from './types'

export const createRandomAccount = ({
solidServer,
Expand Down Expand Up @@ -54,15 +56,26 @@ const finishIntegration = async (verificationLink: string) => {

export const verifyEmail = async ({
email,
person,
authenticatedFetch,
}: {
email: string
person: Person
authenticatedFetch: typeof fetch
}) => {
await setupEmailSettings({
person,
email: '',
emailVerificationToken: '',
authenticatedFetch,
skipSettings: true,
})

const { verificationLink } = await initIntegration({
email,
authenticatedFetch,
})

const { token } = await finishIntegration(verificationLink)

return token
Expand Down
Loading