-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added VcIssuer and builders related to that
- Loading branch information
Showing
19 changed files
with
702 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { parse } from 'querystring'; | ||
|
||
import jwt_decode from 'jwt-decode'; | ||
|
||
import { BAD_PARAMS, JWTHeader } from '../index'; | ||
|
||
export function getKidFromJWT(jwt: string): string { | ||
const header: JWTHeader = jwt_decode(jwt); | ||
return header.kid as string; | ||
} | ||
|
||
export function decodeUriAsJson(uri: string) { | ||
if (!uri) { | ||
throw new Error(BAD_PARAMS); | ||
} | ||
const queryString = uri.replace(/^([a-zA-Z-_]+:\/\/[?]?)/g, ''); | ||
if (!queryString) { | ||
throw new Error(BAD_PARAMS); | ||
} | ||
const parts = parse(queryString); | ||
|
||
const json = {}; | ||
for (const key in parts) { | ||
const value = parts[key]; | ||
if (!value) { | ||
continue; | ||
} | ||
const isBool = typeof value === 'boolean'; | ||
const isNumber = typeof value === 'number'; | ||
const isString = typeof value == 'string'; | ||
|
||
if (isBool || isNumber) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
json[decodeURIComponent(key)] = value; | ||
} else if (isString) { | ||
const decoded = decodeURIComponent(value); | ||
if (decoded.startsWith('{') && decoded.endsWith('}')) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
json[decodeURIComponent(key)] = JSON.parse(decoded); | ||
} else { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
json[decodeURIComponent(key)] = decoded; | ||
} | ||
} | ||
} | ||
return json; | ||
} | ||
|
||
export function encodeJsonAsURI(json: unknown): string { | ||
if (typeof json === 'string') { | ||
return encodeJsonAsURI(JSON.parse(json)); | ||
} | ||
|
||
const results: string[] = []; | ||
|
||
function encodeAndStripWhitespace(key: string): string { | ||
return encodeURIComponent(key.replace(' ', '')); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
for (const [key, value] of Object.entries(json)) { | ||
if (!value) { | ||
continue; | ||
} | ||
const isBool = typeof value == 'boolean'; | ||
const isNumber = typeof value == 'number'; | ||
const isString = typeof value == 'string'; | ||
let encoded; | ||
if (isBool || isNumber) { | ||
encoded = `${encodeAndStripWhitespace(key)}=${value}`; | ||
} else if (isString) { | ||
encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(value)}`; | ||
} else { | ||
encoded = `${encodeAndStripWhitespace(key)}=${encodeURIComponent(JSON.stringify(value))}`; | ||
} | ||
results.push(encoded); | ||
} | ||
return results.join('&'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './CredentialOfferUtil'; | ||
export * from './Encoding'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,4 @@ | |
"esModuleInterop": true, | ||
"moduleResolution": "Node" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import {CredentialRequest, CredentialResponse, getKidFromJWT, IssuerMetadata, TokenErrorResponse} from "@sphereon/openid4vci-common"; | ||
import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' | ||
|
||
export class VcIssuer { | ||
_issuerMetadata: IssuerMetadata | ||
_userPinRequired?: boolean | ||
constructor(issuerMetadata: IssuerMetadata, userPinRequired?: boolean) { | ||
this._issuerMetadata = issuerMetadata | ||
this._userPinRequired = userPinRequired | ||
} | ||
|
||
public getIssuerMetadata() { | ||
return this._issuerMetadata | ||
} | ||
|
||
public async issueCredentialFromIssueRequest(issueCredentialRequest: CredentialRequest): Promise<CredentialResponse> { | ||
//TODO: do we want additional validations here? | ||
if (this.isMetadataSupportCredentialRequestFormat(issueCredentialRequest.format)) { | ||
return await this.issueCredential(issueCredentialRequest) | ||
} | ||
throw new Error(TokenErrorResponse.invalid_request) | ||
} | ||
|
||
private isMetadataSupportCredentialRequestFormat(requestFormat: string | string[]): boolean { | ||
for (const credentialSupported of this._issuerMetadata.credentials_supported) { | ||
if (!Array.isArray(requestFormat) && credentialSupported.format === requestFormat) { | ||
return true | ||
} | ||
else if (Array.isArray(requestFormat)) { | ||
for (const format of requestFormat as string[]) { | ||
if (credentialSupported.format === format) { | ||
return true | ||
} | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
private async issueCredential(issueCredentialRequest: CredentialRequest): Promise<CredentialResponse> { | ||
const credential: ICredential = { | ||
'@context': ['https://www.w3.org/2018/credentials/v1'], | ||
issuanceDate: new Date().toUTCString(), | ||
issuer: process.env.issuer_did as string, | ||
type: Array.isArray(issueCredentialRequest.type)?issueCredentialRequest.type: [issueCredentialRequest.type], | ||
credentialSubject: { | ||
id: getKidFromJWT(issueCredentialRequest.proof.jwt as string), | ||
given_name: 'John Doe', | ||
}, | ||
} | ||
return { | ||
//todo: sign the credential here | ||
credential: credential as W3CVerifiableCredential, | ||
format: issueCredentialRequest.format, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { | ||
CredentialFormatEnum, | ||
CredentialIssuerMetadataSupportedCredentials, | ||
Display, | ||
IssuerCredentialSubjectDisplay, | ||
TokenErrorResponse | ||
} from '@sphereon/openid4vci-common' | ||
|
||
import { CredentialSupportedV1_11Builder, VcIssuerBuilder } from '../index' | ||
|
||
describe('VcIssuer builder should', () => { | ||
it('generate a VcIssuer', () => { | ||
const credentialsSupported: CredentialIssuerMetadataSupportedCredentials = new CredentialSupportedV1_11Builder() | ||
.withCryptographicSuitesSupported('ES256K') | ||
.withCryptographicBindingMethod('did') | ||
.withFormat(CredentialFormatEnum.jwt_vc_json) | ||
.withId('UniversityDegree_JWT') | ||
.withCredentialDisplay({ | ||
name: 'University Credential', | ||
locale: 'en-US', | ||
logo: { | ||
url: 'https://exampleuniversity.com/public/logo.png', | ||
alt_text: 'a square logo of a university', | ||
}, | ||
background_color: '#12107c', | ||
text_color: '#FFFFFF', | ||
} as Display) | ||
.withIssuerCredentialSubjectDisplay('given_name', { | ||
name: 'given name', | ||
locale: 'en-US', | ||
} as IssuerCredentialSubjectDisplay) | ||
.build() | ||
const vcIssuer = new VcIssuerBuilder() | ||
.withAuthorizationServer('https://authorization-server') | ||
.withCredentialEndpoint('https://credential-endpoint') | ||
.withCredentialIssuer('https://credential-issuer') | ||
.withIssuerDisplay({ | ||
name: 'example issuer', | ||
locale: 'en-US', | ||
}) | ||
.withCredentialsSupported(credentialsSupported) | ||
.build() | ||
|
||
expect(vcIssuer.getIssuerMetadata().authorization_server).toEqual('https://authorization-server') | ||
expect(vcIssuer.getIssuerMetadata().display).toBeDefined() | ||
expect(vcIssuer.getIssuerMetadata().credentials_supported[0].id).toEqual('UniversityDegree_JWT') | ||
}) | ||
|
||
it('fail to generate a VcIssuer', () => { | ||
const credentialsSupported: CredentialIssuerMetadataSupportedCredentials = new CredentialSupportedV1_11Builder() | ||
.withCryptographicSuitesSupported('ES256K') | ||
.withCryptographicBindingMethod('did') | ||
.withFormat(CredentialFormatEnum.jwt_vc_json) | ||
.withId('UniversityDegree_JWT') | ||
.withCredentialDisplay({ | ||
name: 'University Credential', | ||
locale: 'en-US', | ||
logo: { | ||
url: 'https://exampleuniversity.com/public/logo.png', | ||
alt_text: 'a square logo of a university', | ||
}, | ||
background_color: '#12107c', | ||
text_color: '#FFFFFF', | ||
} as Display) | ||
.withIssuerCredentialSubjectDisplay('given_name', { | ||
name: 'given name', | ||
locale: 'en-US', | ||
} as IssuerCredentialSubjectDisplay) | ||
.build() | ||
expect(() => | ||
new VcIssuerBuilder() | ||
.withAuthorizationServer('https://authorization-server') | ||
.withCredentialEndpoint('https://credential-endpoint') | ||
.withIssuerDisplay({ | ||
name: 'example issuer', | ||
locale: 'en-US', | ||
}) | ||
.withCredentialsSupported(credentialsSupported) | ||
.build() | ||
).toThrowError(TokenErrorResponse.invalid_request) | ||
}) | ||
|
||
it('fail to generate a CredentialSupportedV1_11', () => { | ||
expect(() => | ||
new CredentialSupportedV1_11Builder() | ||
.withCryptographicSuitesSupported('ES256K') | ||
.withCryptographicBindingMethod('did') | ||
.withId('UniversityDegree_JWT') | ||
.build() | ||
).toThrowError(TokenErrorResponse.invalid_request) | ||
}) | ||
}) |
Oops, something went wrong.