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 2 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, 'decryptJwt' | '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' | 'decryptJwt' | 'encryptJwe'>
}

export class Oauth2Client {
Expand Down
52 changes: 51 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/v-jwk'
import type { JwtHeader, JwtPayload, JwtSigner } from './common/jwt/v-jwt'
import type { JweEncryptor, JwtHeader, JwtPayload, JwtSigner } from './common/jwt/v-jwt'

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

export interface DecryptJwtCallbackOptions {
jwk: Jwk
}

export type DecryptJwtCallback = (
jwe: string,
options?: DecryptJwtCallbackOptions
) => OrPromise<
| {
decrypted: true
decryptionJwk: Jwk
payload: string
header: JwtHeader
}
| {
decrypted: false
decryptionJwk?: Jwk
payload?: string
header?: JwtHeader
}
>

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 +88,16 @@ export interface CallbackContext {
*/
signJwt: SignJwtCallback

/**
* Decrypt jwe callback for decrypting of Json Web Encryptions
*/
decryptJwt: DecryptJwtCallback

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

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

/**
* Get the DNS names from a X.509 certificate
*/
getX509SanDnsNames?: (certificate: string) => string[]

/**
* Get the URI names from a X.509 certificate
*/
getX509SanUriNames?: (certificate: string) => string[]
}
6 changes: 6 additions & 0 deletions packages/oauth2/src/common/jwe/v-jwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as v from 'valibot'

export const vCompactJwe = v.pipe(
v.string(),
v.regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, 'Not a valid compact jwe')
)
2 changes: 1 addition & 1 deletion packages/oauth2/src/common/jwk/v-jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const vJwk = v.looseObject({
q: v.optional(v.string()),
qi: v.optional(v.string()),
use: v.optional(v.string()),
x5c: v.optional(v.string()),
x5c: v.optional(v.array(v.string())),
x5t: v.optional(v.string()),
'x5t#S256': v.optional(v.string()),
x5u: v.optional(v.string()),
Expand Down
52 changes: 52 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,52 @@
import {
type BaseSchema,
decodeBase64,
encodeToUtf8String,
parseWithErrorHandling,
stringToJsonWithErrorHandling,
} from '@openid4vc/utils'
import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError'
import type { InferSchemaOutput } from './decode-jwt'
import { vJwtHeader } from './v-jwt'

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

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

export type DecodeJweResult<HeaderSchema extends BaseSchema | undefined = undefined> = {
header: InferSchemaOutput<HeaderSchema, typeof vJwtHeader>
}

export function decodeJwtHeader<HeaderSchema extends BaseSchema | undefined = undefined>(
options: DecodeJwtHeaderOptions<HeaderSchema>
): DecodeJweResult<HeaderSchema> {
const jwtParts = options.jwe.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')
}

const header = parseWithErrorHandling(options.headerSchema ?? vJwtHeader, headerJson)

return {
header,
}
}
12 changes: 4 additions & 8 deletions packages/oauth2/src/common/jwt/decode-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
stringToJsonWithErrorHandling,
} from '@openid4vc/utils'
import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError'
import { type JwtSigner, vJwtHeader, vJwtPayload } from './v-jwt'
import { decodeJwtHeader } from './decode-jwt-header'
import { type JwtSigner, type vJwtHeader, vJwtPayload } from './v-jwt'

export interface DecodeJwtOptions<
HeaderSchema extends BaseSchema | undefined,
Expand Down Expand Up @@ -50,13 +51,8 @@ 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'
Expand All @@ -65,7 +61,7 @@ export function decodeJwt<
throw new Oauth2JwtParseError('Error parsing JWT')
}

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

return {
Expand Down Expand Up @@ -175,7 +171,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 InferSchemaOutput<
ProvidedSchema extends BaseSchema | undefined,
DefaultSchema extends BaseSchema,
> = IsSchemaProvided<ProvidedSchema> extends true
Expand Down
6 changes: 6 additions & 0 deletions packages/oauth2/src/common/jwt/v-jwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as v from 'valibot'

export const vCompactJwe = v.pipe(
v.string(),
v.regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, 'Not a valid compact jwe')
)
6 changes: 6 additions & 0 deletions packages/oauth2/src/common/jwt/v-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export type JwtSignerCustom = {

export type JwtSigner = JwtSignerDid | JwtSignerJwk | JwtSignerX5c | JwtSignerTrustChain | JwtSignerCustom

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

export type JwtSignerWithJwk = JwtSigner & { publicJwk: Jwk }

export const vCompactJwt = v.pipe(
Expand Down
6 changes: 6 additions & 0 deletions packages/oauth2/src/common/v-oauth2-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ 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',
}

export const vOauth2ErrorResponse = v.looseObject({
Expand Down
Loading