@sphereon/ssi-sdk.siopv2-oid4vp-rp-rest-api
Version:
185 lines (164 loc) • 8.54 kB
text/typescript
import { AuthorizationResponsePayload, PresentationSubmission } from '@sphereon/did-auth-siop'
import { checkAuth, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support'
import { AuthorizationChallengeValidationResponse } from '@sphereon/ssi-sdk.siopv2-oid4vp-common'
import { CredentialMapper } from '@sphereon/ssi-types'
import { Request, Response, Router } from 'express'
import { validate as isValidUUID } from 'uuid'
import { IRequiredContext } from './types'
import { DcqlQuery } from 'dcql'
const parseAuthorizationResponse = (request: Request): AuthorizationResponsePayload => {
const contentType = request.header('content-type')
if (contentType?.startsWith('application/json')) {
const payload = typeof request.body === 'string' ? JSON.parse(request.body) : request.body
return payload as AuthorizationResponsePayload
}
if (contentType?.startsWith('application/x-www-form-urlencoded')) {
const payload = request.body as AuthorizationResponsePayload
// Parse presentation_submission if it's a string
if (typeof payload.presentation_submission === 'string') {
console.log(`Supplied presentation_submission was a string instead of JSON. Correcting, but external party should fix their implementation!`)
payload.presentation_submission = JSON.parse(payload.presentation_submission)
}
// when using FORM_URL_ENCODED, vp_token comes back as string not matter whether the input was string, object or array. Handled below.
if (typeof payload.vp_token === 'string') {
const { vp_token } = payload
// The only use case where vp_object is an object is JsonLdAsString atm. For arrays, any objects will be parsed along with the array
// (Leaving the vp_token JsonLdAsString causes problems because the original credential will remain string and will be interpreted as JWT in some parts of the code)
if ((vp_token.startsWith('[') && vp_token.endsWith(']')) || CredentialMapper.isJsonLdAsString(vp_token)) {
payload.vp_token = JSON.parse(vp_token)
}
}
return payload
}
throw new Error(
`Unsupported content type: ${contentType}. Currently only application/x-www-form-urlencoded and application/json (for direct_post) are supported`,
)
}
const validatePresentationSubmission = (query: DcqlQuery, submission: PresentationSubmission): boolean => {
return query.credentials.every((credential) => credential.id in submission)
}
export function verifyAuthResponseSIOPv2Endpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) {
if (opts?.enabled === false) {
console.log(`verifyAuthResponse SIOP endpoint is disabled`)
return
}
const path = opts?.path ?? '/siop/queries/:queryId/auth-responses/:correlationId'
router.post(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => {
try {
const { correlationId, queryId, tenantId, version } = request.params
if (!correlationId) {
console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}`)
return sendErrorResponse(response, 404, 'No authorization request could be found')
}
console.debug('Authorization Response (siop-sessions') // TODO use logger
console.debug(JSON.stringify(request.body, null, 2))
const definitionItems = await context.agent.pdmGetDefinitions({
filter: buildQueryIdFilter(queryId, tenantId, version),
})
if (definitionItems.length === 0) {
console.log(`Could not get dcql query with id ${queryId} from agent. Will return 404`)
response.statusCode = 404
response.statusMessage = `No definition ${queryId}`
return response.send()
}
const authorizationResponse = parseAuthorizationResponse(request)
console.log(`URI: ${JSON.stringify(authorizationResponse)}`)
const definitionItem = definitionItems[0]
const verifiedResponse = await context.agent.siopVerifyAuthResponse({
authorizationResponse,
correlationId,
dcqlQuery: definitionItem.query,
})
const presentation = verifiedResponse?.oid4vpSubmission?.presentation
if (presentation && validatePresentationSubmission(definitionItem.query, presentation)) {
console.log('PRESENTATIONS:' + JSON.stringify(presentation, null, 2))
response.statusCode = 200
const authorizationChallengeValidationResponse: AuthorizationChallengeValidationResponse = {
presentation_during_issuance_session: verifiedResponse.correlationId,
}
if (authorizationResponse.is_first_party) {
response.setHeader('Content-Type', 'application/json')
return response.send(JSON.stringify(authorizationChallengeValidationResponse))
}
const responseRedirectURI = await context.agent.siopGetRedirectURI({ correlationId, state: verifiedResponse.state })
if (responseRedirectURI) {
response.setHeader('Content-Type', 'application/json')
return response.send(JSON.stringify({ redirect_uri: responseRedirectURI }))
}
// todo: delete session
} else {
console.log('Missing Presentation (Verifiable Credentials)')
response.statusCode = 500
response.statusMessage = 'Missing Presentation (Verifiable Credentials)'
}
return response.send()
} catch (error) {
console.error(error)
return sendErrorResponse(response, 500, 'Could not verify auth status', error)
}
})
}
export function getAuthRequestSIOPv2Endpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) {
if (opts?.enabled === false) {
console.log(`getAuthRequest SIOP endpoint is disabled`)
return
}
const path = opts?.path ?? '/siop/queries/:queryId/auth-requests/:correlationId'
router.get(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => {
try {
const correlationId = request.params.correlationId
const queryId = request.params.queryId
if (!correlationId || !queryId) {
console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, queryId: ${queryId}`)
return sendErrorResponse(response, 404, 'No authorization request could be found')
}
const requestState = await context.agent.siopGetAuthRequestState({
correlationId,
errorOnNotFound: false,
})
if (!requestState) {
console.log(
`No authorization request could be found for the given url in the state manager. correlationId: ${correlationId}, definitionId: ${queryId}`,
)
return sendErrorResponse(response, 404, `No authorization request could be found`)
}
const definitionItems = await context.agent.pdmGetDefinitions({ filter: buildQueryIdFilter(queryId) })
if (definitionItems.length === 0) {
console.log(`Could not get dcql query with id ${queryId} from agent. Will return 404`)
response.statusCode = 404
response.statusMessage = `No definition ${queryId}`
return response.send()
}
const payload = requestState.request?.requestObject?.getPayload()!
payload.dcql_query = definitionItems[0].query
const requestObject = await requestState.request?.requestObject?.toJwt()
console.log('JWT Request object:')
console.log(requestObject)
let error: string | undefined
try {
response.statusCode = 200
response.setHeader('Content-Type', 'application/jwt')
return response.send(requestObject)
} catch (e) {
error = typeof e === 'string' ? e : e instanceof Error ? e.message : undefined
return sendErrorResponse(response, 500, 'Could not get authorization request', e)
} finally {
await context.agent.siopUpdateAuthRequestState({
correlationId,
state: 'authorization_request_created',
error,
})
}
} catch (error) {
return sendErrorResponse(response, 500, 'Could not get authorization request', error)
}
})
}
export function buildQueryIdFilter(queryId: string, tenantId?: string, version?: string) {
const queryFilter = {
queryId,
...(tenantId ? { tenantId } : {}),
...(version ? { version } : {}),
}
return [queryFilter, ...(isValidUUID(queryId) ? [{ id: queryId }] : [])] // Allow both PK (unique queryId + version combi) or just plain queryId which assumes the latest version
}