@sphereon/ssi-sdk.oid4vci-issuer
Version:
253 lines (234 loc) • 9.85 kB
text/typescript
import {
AccessTokenResponse,
AuthorizationServerMetadata,
CredentialResponse,
IssuerMetadata,
OpenIDResponse,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common'
import { assertValidAccessTokenRequest, createAccessTokenResponse, VcIssuer } from '@sphereon/oid4vci-issuer'
import { retrieveWellknown } from '@sphereon/oid4vci-client'
import { getAgentResolver } from '@sphereon/ssi-sdk-ext.did-utils'
import { IMetadataOptions } from '@sphereon/ssi-sdk.oid4vci-issuer-store'
import { IAgentPlugin } from '@veramo/core'
import { getAccessTokenSignerCallback } from '../functions'
import {
IAssertValidAccessTokenArgs,
ICreateCredentialOfferURIResult,
ICreateOfferArgs,
IIssueCredentialArgs,
IIssuerInstanceArgs,
IIssuerOptions,
IOID4VCIIssuerOpts,
IRequiredContext,
schema,
} from '../index'
import { IssuerInstance } from '../IssuerInstance'
import { IOID4VCIIssuer } from '../types/IOID4VCIIssuer'
export class OID4VCIIssuer implements IAgentPlugin {
private static readonly _DEFAULT_OPTS_KEY = '_default'
private readonly instances: Map<string, IssuerInstance> = new Map()
readonly schema = schema.IDidAuthSiopOpAuthenticator
readonly methods: IOID4VCIIssuer = {
oid4vciCreateOfferURI: this.oid4vciCreateOfferURI.bind(this),
oid4vciIssueCredential: this.oid4vciIssueCredential.bind(this),
oid4vciCreateAccessTokenResponse: this.oid4vciCreateAccessTokenResponse.bind(this),
oid4vciGetInstance: this.oid4vciGetInstance.bind(this),
}
private _opts: IOID4VCIIssuerOpts
constructor(opts?: IOID4VCIIssuerOpts) {
this._opts = opts ?? {}
}
private async oid4vciCreateOfferURI(createArgs: ICreateOfferArgs, context: IRequiredContext): Promise<ICreateCredentialOfferURIResult> {
return await this.oid4vciGetInstance(createArgs, context)
.then((instance) => instance.get({ context }))
.then((issuer: VcIssuer) =>
issuer.createCredentialOfferURI(createArgs).then((response) => {
const result: ICreateCredentialOfferURIResult = response
if (this._opts.returnSessions === false) {
delete result.session
}
return result
}),
)
}
private async oid4vciIssueCredential(issueArgs: IIssueCredentialArgs, context: IRequiredContext): Promise<CredentialResponse> {
return await this.oid4vciGetInstance(issueArgs, context)
.then((instance) => instance.get({ context }))
.then((issuer: VcIssuer) => issuer.issueCredential(issueArgs))
}
private async oid4vciCreateAccessTokenResponse(
accessTokenArgs: IAssertValidAccessTokenArgs,
context: IRequiredContext,
): Promise<AccessTokenResponse> {
return await this.oid4vciGetInstance(accessTokenArgs, context).then(async (instance) => {
const issuer = await instance.get({ context })
await assertValidAccessTokenRequest(accessTokenArgs.request, {
credentialOfferSessions: issuer.credentialOfferSessions,
expirationDuration: accessTokenArgs.expirationDuration,
})
const accessTokenIssuer = instance.issuerOptions.idOpts?.issuer ?? instance.issuerOptions.didOpts?.idOpts.identifier.toString() // last part is legacy
if (!accessTokenIssuer) {
return Promise.reject(Error(`Could not determine access token issuer`))
}
return createAccessTokenResponse(accessTokenArgs.request, {
accessTokenIssuer,
tokenExpiresIn: accessTokenArgs.expirationDuration,
cNonceExpiresIn: accessTokenArgs.expirationDuration,
cNonces: issuer.cNonces,
credentialOfferSessions: issuer.credentialOfferSessions,
accessTokenSignerCallback: await getAccessTokenSignerCallback(instance.issuerOptions, context),
})
})
}
private getExternalAS(issuerMetadata: IssuerMetadata): string | undefined {
if ('authorization_servers' in issuerMetadata && Array.isArray(issuerMetadata.authorization_servers)) {
return issuerMetadata.authorization_servers.find((as) => as !== issuerMetadata.credential_issuer)
}
return undefined
}
private async createIssuerInstance(args: IIssuerInstanceArgs, context: IRequiredContext): Promise<IssuerInstance> {
const credentialIssuer = args.credentialIssuer ?? OID4VCIIssuer._DEFAULT_OPTS_KEY
//todo: prob doesn't make sense as credentialIssuer is mandatory anyway
const metadataOpts = await this.getMetadataOpts({ ...args, credentialIssuer }, context)
const issuerMetadata = await this.getIssuerMetadata({ ...args, credentialIssuer }, context)
const externalAS = this.getExternalAS(issuerMetadata)
let asMetadataResponse: OpenIDResponse<AuthorizationServerMetadata> | undefined = undefined
if (externalAS) {
// Let's try OIDC first and then fallback to OAuth2
asMetadataResponse = await retrieveWellknown(externalAS, WellKnownEndpoints.OPENID_CONFIGURATION, {
errorOnNotFound: false,
})
if (!asMetadataResponse) {
asMetadataResponse = await retrieveWellknown(externalAS, WellKnownEndpoints.OAUTH_AS, {
errorOnNotFound: true,
})
}
}
const authorizationServerMetadata = asMetadataResponse?.successBody
? asMetadataResponse!.successBody
: await this.getAuthorizationServerMetadataFromStore(
{
...args,
credentialIssuer,
},
context,
)
const issuerOpts = await this.getIssuerOptsFromStore({ ...args, credentialIssuer }, context)
if (!issuerOpts.resolveOpts) {
issuerOpts.resolveOpts = { ...issuerOpts.didOpts?.resolveOpts, ...this._opts.resolveOpts }
}
if (!issuerOpts.resolveOpts?.resolver) {
issuerOpts.resolveOpts.resolver = getAgentResolver(context)
}
this.instances.set(
credentialIssuer,
new IssuerInstance({
issuerOpts,
metadataOpts,
issuerMetadata,
authorizationServerMetadata,
}),
)
return this.oid4vciGetInstance(args, context)
}
public async oid4vciGetInstance(args: IIssuerInstanceArgs, context: IRequiredContext): Promise<IssuerInstance> {
const credentialIssuer = args.credentialIssuer ?? OID4VCIIssuer._DEFAULT_OPTS_KEY
//todo: prob doesn't make sense as credentialIssuer is mandatory anyway
if (!this.instances.has(credentialIssuer)) {
await this.createIssuerInstance(args, context)
}
return this.instances.get(credentialIssuer)!
}
private async getIssuerOptsFromStore(
opts: {
credentialIssuer: string
storeId?: string
namespace?: string
},
context: IRequiredContext,
): Promise<IIssuerOptions> {
const credentialIssuer = opts.credentialIssuer
const storeId = await this.storeId(opts, context)
const namespace = await this.namespace(opts, context)
const options = await context.agent.oid4vciStoreGetIssuerOpts({
metadataType: 'issuer',
correlationId: credentialIssuer,
storeId,
namespace,
})
if (!options) {
throw Error(`Could not get specific nor default options for definition ${credentialIssuer}`)
}
return options
}
private async getMetadataOpts(
opts: {
credentialIssuer: string
storeId?: string
namespace?: string
},
context: IRequiredContext,
): Promise<IMetadataOptions> {
const credentialIssuer = opts.credentialIssuer
const storeId = await this.storeId(opts, context)
const storeNamespace = await this.namespace(opts, context)
return { credentialIssuer, storeId, storeNamespace }
}
private async getIssuerMetadata(
opts: {
credentialIssuer: string
storeId?: string
namespace?: string
},
context: IRequiredContext,
): Promise<IssuerMetadata> {
const metadataOpts = await this.getMetadataOpts(opts, context)
const metadata = (await context.agent.oid4vciStoreGetMetadata({
metadataType: 'issuer',
correlationId: metadataOpts.credentialIssuer,
namespace: metadataOpts.storeNamespace,
storeId: metadataOpts.storeId,
})) as IssuerMetadata
if (!metadata) {
throw Error(`Issuer metadata not found for issuer ${opts.credentialIssuer}, namespace ${opts.namespace} and store ${opts.storeId}`)
}
return metadata
}
private async getAuthorizationServerMetadataFromStore(
opts: {
credentialIssuer: string
storeId?: string
namespace?: string
},
context: IRequiredContext,
): Promise<AuthorizationServerMetadata> {
const metadataOpts = await this.getMetadataOpts(opts, context)
const metadata = (await context.agent.oid4vciStoreGetMetadata({
metadataType: 'authorizationServer',
correlationId: metadataOpts.credentialIssuer,
namespace: metadataOpts.storeNamespace,
storeId: metadataOpts.storeId,
})) as AuthorizationServerMetadata
if (!metadata) {
throw Error(
`Authorization server ${opts.credentialIssuer} metadata not found for namespace ${metadataOpts.storeNamespace} and store ${metadataOpts.storeId}`,
)
}
return metadata
}
private async storeId(opts?: { storeId?: string }, context?: IRequiredContext): Promise<string> {
const storeId = opts?.storeId ?? this._opts?.defaultStoreId ?? (await context?.agent.oid4vciStoreDefaultStoreId())
if (!storeId) {
throw Error('Please provide a store id a default value, or provide the context for a global default store id')
}
return storeId
}
private async namespace(opts?: { namespace?: string }, context?: IRequiredContext): Promise<string> {
const namespace = opts?.namespace ?? this._opts?.defaultNamespace ?? (await context?.agent.oid4vciStoreDefaultNamespace())
if (!namespace) {
throw Error('Please provide a namespace a default value, or provide the context for a global default namespace')
}
return namespace
}
}