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

feat: openid4vp alpha #21

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion packages/oauth2/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@openid4vc/oauth2",
"version": "0.2.0",
"exports": "./src/index.ts",
"files": ["dist"],
"license": "Apache-2.0",
"exports": "./src/index.ts",
"homepage": "https://github.com/openwallet-foundation-labs/oid4vc-ts/tree/main/packages/oauth2",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/oauth2/src/Oauth2AuthorizationServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface Oauth2AuthorizationServerOptions {
/**
* Callbacks required for the oauth2 authorization server
*/
callbacks: CallbackContext
callbacks: Omit<CallbackContext, 'decryptJwe' | 'encryptJwe'>
}

export class Oauth2AuthorizationServer {
Expand Down
2 changes: 1 addition & 1 deletion packages/oauth2/src/Oauth2Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface Oauth2ClientOptions {
/**
* Callbacks required for the oauth2 client
*/
callbacks: Omit<CallbackContext, 'verifyJwt' | 'clientAuthentication'>
callbacks: Omit<CallbackContext, 'verifyJwt' | 'clientAuthentication' | 'decryptJwe' | 'encryptJwe'>
}

export class Oauth2Client {
Expand Down
48 changes: 47 additions & 1 deletion packages/oauth2/src/callbacks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Fetch, OrPromise } from '@openid4vc/utils'
import type { ClientAuthenticationCallback } from './client-authentication'
import type { Jwk } from './common/jwk/z-jwk'
import type { JwtHeader, JwtPayload, JwtSigner } from './common/jwt/z-jwt'
import type { JweEncryptor, JwtHeader, JwtPayload, JwtSigner } from './common/jwt/z-jwt'

/**
* Supported hashing algorithms
Expand Down Expand Up @@ -39,6 +39,34 @@ export type VerifyJwtCallback = (
}
>

export interface DecryptJweCallbackOptions {
jwk: Jwk
}

export type DecryptJweCallback = (
jwe: string,
options?: DecryptJweCallbackOptions
) => OrPromise<
| {
decrypted: true
decryptionJwk: Jwk
payload: string
}
| {
decrypted: false
decryptionJwk?: Jwk
payload?: string
}
>

export type EncryptJweCallback = (
jweEncryptor: JweEncryptor,
data: string
) => OrPromise<{
encryptionJwk: Jwk
jwe: string
}>

/**
* Callback context provides the callbacks that are required for the oid4vc library
*/
Expand All @@ -58,6 +86,16 @@ export interface CallbackContext {
*/
signJwt: SignJwtCallback

/**
* Decrypt jwe callback for decrypting of Json Web Encryptions
*/
decryptJwe: DecryptJweCallback

/**
* Encrypt jwt callback for encrypting of Json Web Encryptions
*/
encryptJwe: EncryptJweCallback

/**
* Verify jwt callback for verification of Json Web Tokens
*/
Expand All @@ -83,4 +121,12 @@ export interface CallbackContext {
* scenarios where multiple authorization servers are supported.
*/
clientAuthentication: ClientAuthenticationCallback

/**
* Get the DNS names and URI names from a X.509 certificate
*/
getX509CertificateMetadata?: (certificate: string) => {
sanDnsNames: string[]
sanUriNames: string[]
}
}
2 changes: 1 addition & 1 deletion packages/oauth2/src/common/jwk/z-jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const zJwk = z
q: z.optional(z.string()),
qi: z.optional(z.string()),
use: z.optional(z.string()),
x5c: z.optional(z.string()),
x5c: z.optional(z.array(z.string())),
x5t: z.optional(z.string()),
'x5t#S256': z.optional(z.string()),
x5u: z.optional(z.string()),
Expand Down
55 changes: 55 additions & 0 deletions packages/oauth2/src/common/jwt/decode-jwt-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
type BaseSchema,
decodeBase64,
encodeToUtf8String,
parseWithErrorHandling,
stringToJsonWithErrorHandling,
} from '@openid4vc/utils'
import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError'
import type { InferSchemaOrDefaultOutput } from './decode-jwt'
import { zJwtHeader } from './z-jwt'

export interface DecodeJwtHeaderOptions<HeaderSchema extends BaseSchema | undefined> {
/**
* The comapct encoded jwt
*/
jwt: string

/**
* Schema to use for validating the header. If not provided the
* default `vJwtHeader` schema will be used
*/
headerSchema?: HeaderSchema
}

export type DecodeJwtHeaderResult<HeaderSchema extends BaseSchema | undefined = undefined> = {
header: InferSchemaOrDefaultOutput<HeaderSchema, typeof zJwtHeader>
}

export function decodeJwtHeader<HeaderSchema extends BaseSchema | undefined = undefined>(
options: DecodeJwtHeaderOptions<HeaderSchema>
): DecodeJwtHeaderResult<HeaderSchema> {
const jwtParts = options.jwt.split('.')
if (jwtParts.length <= 2) {
throw new Oauth2JwtParseError('Jwt is not a valid jwt, unable to decode')
}

let headerJson: Record<string, unknown>
try {
headerJson = stringToJsonWithErrorHandling(
encodeToUtf8String(decodeBase64(jwtParts[0])),
'Unable to parse jwt header to JSON'
)
} catch (error) {
throw new Oauth2JwtParseError(`Error parsing JWT. ${error instanceof Error ? error.message : ''}`)
}

const header = parseWithErrorHandling(options.headerSchema ?? zJwtHeader, headerJson) as InferSchemaOrDefaultOutput<
HeaderSchema,
typeof zJwtHeader
>

return {
header,
}
}
23 changes: 9 additions & 14 deletions packages/oauth2/src/common/jwt/decode-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
} from '@openid4vc/utils'
import type z from 'zod'
import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError'
import { type JwtSigner, zJwtHeader, zJwtPayload } from './z-jwt'

import { decodeJwtHeader } from './decode-jwt-header'
import { type JwtSigner, type zJwtHeader, zJwtPayload } from './z-jwt'
export interface DecodeJwtOptions<
HeaderSchema extends BaseSchema | undefined,
PayloadSchema extends BaseSchema | undefined,
Expand All @@ -35,8 +35,8 @@ export type DecodeJwtResult<
HeaderSchema extends BaseSchema | undefined = undefined,
PayloadSchema extends BaseSchema | undefined = undefined,
> = {
header: InferSchemaOutput<HeaderSchema, typeof zJwtHeader>
payload: InferSchemaOutput<PayloadSchema, typeof zJwtPayload>
header: InferSchemaOrDefaultOutput<HeaderSchema, typeof zJwtHeader>
payload: InferSchemaOrDefaultOutput<PayloadSchema, typeof zJwtPayload>
signature: string
}

Expand All @@ -49,27 +49,22 @@ export function decodeJwt<
throw new Oauth2JwtParseError('Jwt is not a valid jwt, unable to decode')
}

let headerJson: Record<string, unknown>
let payloadJson: Record<string, unknown>
try {
headerJson = stringToJsonWithErrorHandling(
encodeToUtf8String(decodeBase64(jwtParts[0])),
'Unable to parse jwt header to JSON'
)
payloadJson = stringToJsonWithErrorHandling(
encodeToUtf8String(decodeBase64(jwtParts[1])),
'Unable to parse jwt payload to JSON'
)
} catch (error) {
throw new Oauth2JwtParseError('Error parsing JWT')
throw new Oauth2JwtParseError(`Error parsing JWT. ${error instanceof Error ? error.message : ''}`)
}

const header = parseWithErrorHandling(options.headerSchema ?? zJwtHeader, headerJson)
const { header } = decodeJwtHeader({ jwt: options.jwt, headerSchema: options.headerSchema })
const payload = parseWithErrorHandling(options.payloadSchema ?? zJwtPayload, payloadJson)

return {
header: header as InferSchemaOutput<HeaderSchema, typeof zJwtHeader>,
payload: payload as InferSchemaOutput<PayloadSchema, typeof zJwtPayload>,
header: header as InferSchemaOrDefaultOutput<HeaderSchema, typeof zJwtHeader>,
payload: payload as InferSchemaOrDefaultOutput<PayloadSchema, typeof zJwtPayload>,
signature: jwtParts[2],
}
}
Expand Down Expand Up @@ -174,7 +169,7 @@ export function jwtSignerFromJwt({ header, payload }: Pick<DecodeJwtResult, 'hea
type IsSchemaProvided<T> = T extends undefined ? false : true

// Helper type to infer the output type based on whether a schema is provided
type InferSchemaOutput<
export type InferSchemaOrDefaultOutput<
ProvidedSchema extends BaseSchema | undefined,
DefaultSchema extends BaseSchema,
> = IsSchemaProvided<ProvidedSchema> extends true
Expand Down
13 changes: 13 additions & 0 deletions packages/oauth2/src/common/jwt/verify-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface VerifyJwtOptions {
* Expected value for the 'sub' claim
*/
expectedSubject?: string

/**
* The claims that are required to be present in the jwt.
*/
requiredClaims?: string[]
}

export interface VerifyJwtReturn {
Expand Down Expand Up @@ -127,6 +132,14 @@ export async function verifyJwt(options: VerifyJwtOptions): Promise<VerifyJwtRet
throw new Oauth2JwtVerificationError(`${errorMessage} jwt 'sub' does not match expected value.`)
}

if (options.requiredClaims) {
for (const claim of options.requiredClaims) {
if (!options.payload[claim]) {
throw new Oauth2JwtVerificationError(`${errorMessage} jwt '${claim}' is missing.`)
}
}
}

return {
signer: {
...options.signer,
Expand Down
7 changes: 7 additions & 0 deletions packages/oauth2/src/common/jwt/z-jwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod'

export const zCompactJwe = z
.string()
.regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, {
message: 'Not a valid compact jwe',
})
6 changes: 6 additions & 0 deletions packages/oauth2/src/common/jwt/z-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export type JwtSigner = JwtSignerDid | JwtSignerJwk | JwtSignerX5c | JwtSignerTr

export type JwtSignerWithJwk = JwtSigner & { publicJwk: Jwk }

export type JweEncryptor = JwtSignerJwk & {
enc: string
apu?: string
apv?: string
}

export const zCompactJwt = z.string().regex(/^([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)$/, {
message: 'Not a valid compact jwt',
})
Expand Down
15 changes: 15 additions & 0 deletions packages/oauth2/src/common/z-oauth2-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ export enum Oauth2ErrorCodes {
InvalidProof = 'invalid_proof',
InvalidNonce = 'invalid_nonce',
InvalidEncryptionParameters = 'invalid_encryption_parameters',

// Jar
InvalidRequestUri = 'invalid_request_uri',
InvalidRequestObject = 'invalid_request_object',
RequestNotSupported = 'request_not_supported',
RequestUriNotSupported = 'request_uri_not_supported',

// OpenId4Vp
VpFormatsNotSupported = 'vp_formats_not_supported',
AccessDenied = 'access_denied',
InvalidPresentationDefinitionUri = 'invalid_presentation_definition_uri',
InvalidPresentationDefinitionReference = 'invalid_presentation_definition_reference',
InvalidRequestUriMethod = 'invalid_request_uri_method',
InvalidTransactionData = 'invalid_transaction_data',
WalletUnavailable = 'wallet_unavailable',
}

export const zOauth2ErrorResponse = z
Expand Down
Loading
Loading