@atproto/jwk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/jwt.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod'
2
-
3
2
  import { jwkPubSchema } from './jwk.js'
4
3
  import { jwtCharsRefinement, segmentedStringRefinementFactory } from './util.js'
5
4
 
@@ -24,150 +23,154 @@ export const isUnsignedJwt = (data: unknown): data is UnsignedJwt =>
24
23
  /**
25
24
  * @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4}
26
25
  */
27
- export const jwtHeaderSchema = z.object({
28
- /** "alg" (Algorithm) Header Parameter */
29
- alg: z.string(),
30
- /** "jku" (JWK Set URL) Header Parameter */
31
- jku: z.string().url().optional(),
32
- /** "jwk" (JSON Web Key) Header Parameter */
33
- jwk: z
34
- .object({
35
- kty: z.string(),
36
- crv: z.string().optional(),
37
- x: z.string().optional(),
38
- y: z.string().optional(),
39
- e: z.string().optional(),
40
- n: z.string().optional(),
41
- })
42
- .optional(),
43
- /** "kid" (Key ID) Header Parameter */
44
- kid: z.string().optional(),
45
- /** "x5u" (X.509 URL) Header Parameter */
46
- x5u: z.string().optional(),
47
- /** "x5c" (X.509 Certificate Chain) Header Parameter */
48
- x5c: z.array(z.string()).optional(),
49
- /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */
50
- x5t: z.string().optional(),
51
- /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */
52
- 'x5t#S256': z.string().optional(),
53
- /** "typ" (Type) Header Parameter */
54
- typ: z.string().optional(),
55
- /** "cty" (Content Type) Header Parameter */
56
- cty: z.string().optional(),
57
- /** "crit" (Critical) Header Parameter */
58
- crit: z.array(z.string()).optional(),
59
- })
26
+ export const jwtHeaderSchema = z
27
+ .object({
28
+ /** "alg" (Algorithm) Header Parameter */
29
+ alg: z.string(),
30
+ /** "jku" (JWK Set URL) Header Parameter */
31
+ jku: z.string().url().optional(),
32
+ /** "jwk" (JSON Web Key) Header Parameter */
33
+ jwk: z
34
+ .object({
35
+ kty: z.string(),
36
+ crv: z.string().optional(),
37
+ x: z.string().optional(),
38
+ y: z.string().optional(),
39
+ e: z.string().optional(),
40
+ n: z.string().optional(),
41
+ })
42
+ .optional(),
43
+ /** "kid" (Key ID) Header Parameter */
44
+ kid: z.string().optional(),
45
+ /** "x5u" (X.509 URL) Header Parameter */
46
+ x5u: z.string().optional(),
47
+ /** "x5c" (X.509 Certificate Chain) Header Parameter */
48
+ x5c: z.array(z.string()).optional(),
49
+ /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */
50
+ x5t: z.string().optional(),
51
+ /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */
52
+ 'x5t#S256': z.string().optional(),
53
+ /** "typ" (Type) Header Parameter */
54
+ typ: z.string().optional(),
55
+ /** "cty" (Content Type) Header Parameter */
56
+ cty: z.string().optional(),
57
+ /** "crit" (Critical) Header Parameter */
58
+ crit: z.array(z.string()).optional(),
59
+ })
60
+ .passthrough()
60
61
 
61
62
  export type JwtHeader = z.infer<typeof jwtHeaderSchema>
62
63
 
63
64
  // https://www.iana.org/assignments/jwt/jwt.xhtml
64
- export const jwtPayloadSchema = z.object({
65
- iss: z.string().optional(),
66
- aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(),
67
- sub: z.string().optional(),
68
- exp: z.number().int().optional(),
69
- nbf: z.number().int().optional(),
70
- iat: z.number().int().optional(),
71
- jti: z.string().optional(),
72
- htm: z.string().optional(),
73
- htu: z.string().optional(),
74
- ath: z.string().optional(),
75
- acr: z.string().optional(),
76
- azp: z.string().optional(),
77
- amr: z.array(z.string()).optional(),
78
- // https://datatracker.ietf.org/doc/html/rfc7800
79
- cnf: z
80
- .object({
81
- kid: z.string().optional(), // Key ID
82
- jwk: jwkPubSchema.optional(), // JWK
83
- jwe: z.string().optional(), // Encrypted key
84
- jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided)
85
-
86
- // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1
87
- jkt: z.string().optional(),
88
-
89
- // https://datatracker.ietf.org/doc/html/rfc8705
90
- 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint
91
-
92
- // https://datatracker.ietf.org/doc/html/rfc9203
93
- osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation
94
- })
95
- .optional(),
96
-
97
- client_id: z.string().optional(),
98
-
99
- scope: z.string().optional(),
100
- nonce: z.string().optional(),
101
-
102
- at_hash: z.string().optional(),
103
- c_hash: z.string().optional(),
104
- s_hash: z.string().optional(),
105
- auth_time: z.number().int().optional(),
106
-
107
- // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
108
-
109
- // OpenID: "profile" scope
110
- name: z.string().optional(),
111
- family_name: z.string().optional(),
112
- given_name: z.string().optional(),
113
- middle_name: z.string().optional(),
114
- nickname: z.string().optional(),
115
- preferred_username: z.string().optional(),
116
- gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values
117
- picture: z.string().url().optional(),
118
- profile: z.string().url().optional(),
119
- website: z.string().url().optional(),
120
- birthdate: z
121
- .string()
122
- .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD
123
- .optional(),
124
- zoneinfo: z
125
- .string()
126
- .regex(/^[A-Za-z0-9_/]+$/)
127
- .optional(),
128
- locale: z
129
- .string()
130
- .regex(/^[a-z]{2}(-[A-Z]{2})?$/)
131
- .optional(),
132
- updated_at: z.number().int().optional(),
133
-
134
- // OpenID: "email" scope
135
- email: z.string().optional(),
136
- email_verified: z.boolean().optional(),
137
-
138
- // OpenID: "phone" scope
139
- phone_number: z.string().optional(),
140
- phone_number_verified: z.boolean().optional(),
141
-
142
- // OpenID: "address" scope
143
- // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
144
- address: z
145
- .object({
146
- formatted: z.string().optional(),
147
- street_address: z.string().optional(),
148
- locality: z.string().optional(),
149
- region: z.string().optional(),
150
- postal_code: z.string().optional(),
151
- country: z.string().optional(),
152
- })
153
- .optional(),
154
-
155
- // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2
156
- authorization_details: z
157
- .array(
158
- z
159
- .object({
160
- type: z.string(),
161
- // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2
162
- locations: z.array(z.string()).optional(),
163
- actions: z.array(z.string()).optional(),
164
- datatypes: z.array(z.string()).optional(),
165
- identifier: z.string().optional(),
166
- privileges: z.array(z.string()).optional(),
167
- })
168
- .passthrough(),
169
- )
170
- .optional(),
171
- })
65
+ export const jwtPayloadSchema = z
66
+ .object({
67
+ iss: z.string().optional(),
68
+ aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(),
69
+ sub: z.string().optional(),
70
+ exp: z.number().int().optional(),
71
+ nbf: z.number().int().optional(),
72
+ iat: z.number().int().optional(),
73
+ jti: z.string().optional(),
74
+ htm: z.string().optional(),
75
+ htu: z.string().optional(),
76
+ ath: z.string().optional(),
77
+ acr: z.string().optional(),
78
+ azp: z.string().optional(),
79
+ amr: z.array(z.string()).optional(),
80
+ // https://datatracker.ietf.org/doc/html/rfc7800
81
+ cnf: z
82
+ .object({
83
+ kid: z.string().optional(), // Key ID
84
+ jwk: jwkPubSchema.optional(), // JWK
85
+ jwe: z.string().optional(), // Encrypted key
86
+ jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided)
87
+
88
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1
89
+ jkt: z.string().optional(),
90
+
91
+ // https://datatracker.ietf.org/doc/html/rfc8705
92
+ 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint
93
+
94
+ // https://datatracker.ietf.org/doc/html/rfc9203
95
+ osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation
96
+ })
97
+ .optional(),
98
+
99
+ client_id: z.string().optional(),
100
+
101
+ scope: z.string().optional(),
102
+ nonce: z.string().optional(),
103
+
104
+ at_hash: z.string().optional(),
105
+ c_hash: z.string().optional(),
106
+ s_hash: z.string().optional(),
107
+ auth_time: z.number().int().optional(),
108
+
109
+ // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
110
+
111
+ // OpenID: "profile" scope
112
+ name: z.string().optional(),
113
+ family_name: z.string().optional(),
114
+ given_name: z.string().optional(),
115
+ middle_name: z.string().optional(),
116
+ nickname: z.string().optional(),
117
+ preferred_username: z.string().optional(),
118
+ gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values
119
+ picture: z.string().url().optional(),
120
+ profile: z.string().url().optional(),
121
+ website: z.string().url().optional(),
122
+ birthdate: z
123
+ .string()
124
+ .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD
125
+ .optional(),
126
+ zoneinfo: z
127
+ .string()
128
+ .regex(/^[A-Za-z0-9_/]+$/)
129
+ .optional(),
130
+ locale: z
131
+ .string()
132
+ .regex(/^[a-z]{2}(-[A-Z]{2})?$/)
133
+ .optional(),
134
+ updated_at: z.number().int().optional(),
135
+
136
+ // OpenID: "email" scope
137
+ email: z.string().optional(),
138
+ email_verified: z.boolean().optional(),
139
+
140
+ // OpenID: "phone" scope
141
+ phone_number: z.string().optional(),
142
+ phone_number_verified: z.boolean().optional(),
143
+
144
+ // OpenID: "address" scope
145
+ // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
146
+ address: z
147
+ .object({
148
+ formatted: z.string().optional(),
149
+ street_address: z.string().optional(),
150
+ locality: z.string().optional(),
151
+ region: z.string().optional(),
152
+ postal_code: z.string().optional(),
153
+ country: z.string().optional(),
154
+ })
155
+ .optional(),
156
+
157
+ // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2
158
+ authorization_details: z
159
+ .array(
160
+ z
161
+ .object({
162
+ type: z.string(),
163
+ // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2
164
+ locations: z.array(z.string()).optional(),
165
+ actions: z.array(z.string()).optional(),
166
+ datatypes: z.array(z.string()).optional(),
167
+ identifier: z.string().optional(),
168
+ privileges: z.array(z.string()).optional(),
169
+ })
170
+ .passthrough(),
171
+ )
172
+ .optional(),
173
+ })
174
+ .passthrough()
172
175
 
173
176
  export type JwtPayload = z.infer<typeof jwtPayloadSchema>
package/src/key.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { jwkAlgorithms } from './alg.js'
2
2
  import { JwkError } from './errors.js'
3
3
  import { Jwk, jwkSchema } from './jwk.js'
4
- import { VerifyOptions, VerifyPayload, VerifyResult } from './jwt-verify.js'
4
+ import { VerifyOptions, VerifyResult } from './jwt-verify.js'
5
5
  import { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'
6
6
  import { cachedGetter } from './util.js'
7
7
 
8
- export abstract class Key {
9
- constructor(protected readonly jwk: Readonly<Jwk>) {
8
+ const jwkSchemaReadonly = jwkSchema.readonly()
9
+
10
+ export abstract class Key<J extends Jwk = Jwk> {
11
+ constructor(protected readonly jwk: Readonly<J>) {
10
12
  // A key should always be used either for signing or encryption.
11
13
  if (!jwk.use) throw new JwkError('Missing "use" Parameter value')
12
14
  }
@@ -24,25 +26,28 @@ export abstract class Key {
24
26
  return false
25
27
  }
26
28
 
27
- get privateJwk(): Jwk | undefined {
29
+ get privateJwk(): Readonly<J> | undefined {
28
30
  return this.isPrivate ? this.jwk : undefined
29
31
  }
30
32
 
31
33
  @cachedGetter
32
- get publicJwk(): Jwk | undefined {
34
+ get publicJwk():
35
+ | Readonly<Exclude<J, { kty: 'oct' }> & { d?: never }>
36
+ | undefined {
33
37
  if (this.isSymetric) return undefined
34
- if (this.isPrivate) {
35
- const { d: _, ...jwk } = this.jwk as any
36
- return jwk
37
- }
38
- return this.jwk
38
+
39
+ return jwkSchemaReadonly.parse({
40
+ ...this.jwk,
41
+ d: undefined,
42
+ k: undefined,
43
+ }) as Exclude<J, { kty: 'oct' }> & { d?: never }
39
44
  }
40
45
 
41
46
  @cachedGetter
42
- get bareJwk(): Jwk | undefined {
47
+ get bareJwk(): Readonly<Jwk> | undefined {
43
48
  if (this.isSymetric) return undefined
44
49
  const { kty, crv, e, n, x, y } = this.jwk as any
45
- return jwkSchema.parse({ crv, e, kty, n, x, y })
50
+ return jwkSchemaReadonly.parse({ crv, e, kty, n, x, y })
46
51
  }
47
52
 
48
53
  get use() {
@@ -64,7 +69,7 @@ export abstract class Key {
64
69
  }
65
70
 
66
71
  get crv() {
67
- return (this.jwk as { crv: undefined } | Extract<Jwk, { crv: unknown }>).crv
72
+ return (this.jwk as { crv: undefined } | Extract<J, { crv: unknown }>).crv
68
73
  }
69
74
 
70
75
  /**
@@ -73,7 +78,7 @@ export abstract class Key {
73
78
  */
74
79
  @cachedGetter
75
80
  get algorithms(): readonly string[] {
76
- return Array.from(jwkAlgorithms(this.jwk))
81
+ return Object.freeze(Array.from(jwkAlgorithms(this.jwk)))
77
82
  }
78
83
 
79
84
  /**
@@ -86,8 +91,8 @@ export abstract class Key {
86
91
  *
87
92
  * @throws {JwtVerifyError} if the JWT is invalid
88
93
  */
89
- abstract verifyJwt<
90
- P extends VerifyPayload = JwtPayload,
91
- C extends string = string,
92
- >(token: SignedJwt, options?: VerifyOptions<C>): Promise<VerifyResult<P, C>>
94
+ abstract verifyJwt<C extends string = never>(
95
+ token: SignedJwt,
96
+ options?: VerifyOptions<C>,
97
+ ): Promise<VerifyResult<C>>
93
98
  }
package/src/keyset.ts CHANGED
@@ -213,13 +213,10 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
213
213
  }
214
214
  }
215
215
 
216
- async verifyJwt<
217
- P extends Record<string, unknown> = JwtPayload,
218
- C extends string = string,
219
- >(
216
+ async verifyJwt<C extends string = never>(
220
217
  token: SignedJwt,
221
218
  options?: VerifyOptions<C>,
222
- ): Promise<VerifyResult<P, C> & { key: K }> {
219
+ ): Promise<VerifyResult<C> & { key: K }> {
223
220
  const { header } = unsafeDecodeJwt(token)
224
221
  const { kid, alg } = header
225
222
 
@@ -227,7 +224,7 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
227
224
 
228
225
  for (const key of this.list({ kid, alg })) {
229
226
  try {
230
- const result = await key.verifyJwt<P, C>(token, options)
227
+ const result = await key.verifyJwt<C>(token, options)
231
228
  return { ...result, key }
232
229
  } catch (err) {
233
230
  errors.push(err)
package/src/util.ts CHANGED
@@ -5,12 +5,12 @@ import { RefinementCtx, ZodIssueCode } from 'zod'
5
5
  export type Simplify<T> = { [K in keyof T]: T[K] } & {}
6
6
  export type Override<T, V> = Simplify<V & Omit<T, keyof V>>
7
7
 
8
- export type RequiredKey<T, K extends string> = Simplify<
9
- string extends K
10
- ? T
11
- : {
12
- [L in K]: Exclude<L extends keyof T ? T[L] : unknown, undefined>
13
- } & Omit<T, K>
8
+ export type RequiredKey<T, K extends keyof T = never> = Simplify<
9
+ T & {
10
+ [L in K]-?: unknown extends T[L]
11
+ ? NonNullable<unknown> | null
12
+ : Exclude<T[L], undefined>
13
+ }
14
14
  >
15
15
 
16
16
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -0,0 +1 @@
1
+ {"root":["./src/alg.ts","./src/errors.ts","./src/index.ts","./src/jwk.ts","./src/jwks.ts","./src/jwt-decode.ts","./src/jwt-verify.ts","./src/jwt.ts","./src/key.ts","./src/keyset.ts","./src/util.ts"],"version":"5.6.3"}