@_mustachio/openauth 0.6.0
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/dist/esm/client.js +186 -0
- package/dist/esm/css.d.js +0 -0
- package/dist/esm/error.js +73 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/issuer.js +558 -0
- package/dist/esm/jwt.js +16 -0
- package/dist/esm/keys.js +113 -0
- package/dist/esm/pkce.js +35 -0
- package/dist/esm/provider/apple.js +28 -0
- package/dist/esm/provider/arctic.js +43 -0
- package/dist/esm/provider/code.js +58 -0
- package/dist/esm/provider/cognito.js +16 -0
- package/dist/esm/provider/discord.js +15 -0
- package/dist/esm/provider/facebook.js +24 -0
- package/dist/esm/provider/github.js +15 -0
- package/dist/esm/provider/google.js +25 -0
- package/dist/esm/provider/index.js +3 -0
- package/dist/esm/provider/jumpcloud.js +15 -0
- package/dist/esm/provider/keycloak.js +15 -0
- package/dist/esm/provider/linkedin.js +15 -0
- package/dist/esm/provider/m2m.js +17 -0
- package/dist/esm/provider/microsoft.js +24 -0
- package/dist/esm/provider/oauth2.js +119 -0
- package/dist/esm/provider/oidc.js +69 -0
- package/dist/esm/provider/passkey.js +315 -0
- package/dist/esm/provider/password.js +306 -0
- package/dist/esm/provider/provider.js +10 -0
- package/dist/esm/provider/slack.js +15 -0
- package/dist/esm/provider/spotify.js +15 -0
- package/dist/esm/provider/twitch.js +15 -0
- package/dist/esm/provider/x.js +16 -0
- package/dist/esm/provider/yahoo.js +15 -0
- package/dist/esm/random.js +27 -0
- package/dist/esm/storage/aws.js +39 -0
- package/dist/esm/storage/cloudflare.js +42 -0
- package/dist/esm/storage/dynamo.js +116 -0
- package/dist/esm/storage/memory.js +88 -0
- package/dist/esm/storage/storage.js +36 -0
- package/dist/esm/subject.js +7 -0
- package/dist/esm/ui/base.js +407 -0
- package/dist/esm/ui/code.js +151 -0
- package/dist/esm/ui/form.js +43 -0
- package/dist/esm/ui/icon.js +92 -0
- package/dist/esm/ui/passkey.js +329 -0
- package/dist/esm/ui/password.js +338 -0
- package/dist/esm/ui/select.js +187 -0
- package/dist/esm/ui/theme.js +115 -0
- package/dist/esm/util.js +54 -0
- package/dist/types/client.d.ts +466 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/error.d.ts +77 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/issuer.d.ts +465 -0
- package/dist/types/issuer.d.ts.map +1 -0
- package/dist/types/jwt.d.ts +6 -0
- package/dist/types/jwt.d.ts.map +1 -0
- package/dist/types/keys.d.ts +18 -0
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/pkce.d.ts +7 -0
- package/dist/types/pkce.d.ts.map +1 -0
- package/dist/types/provider/apple.d.ts +108 -0
- package/dist/types/provider/apple.d.ts.map +1 -0
- package/dist/types/provider/arctic.d.ts +16 -0
- package/dist/types/provider/arctic.d.ts.map +1 -0
- package/dist/types/provider/code.d.ts +74 -0
- package/dist/types/provider/code.d.ts.map +1 -0
- package/dist/types/provider/cognito.d.ts +64 -0
- package/dist/types/provider/cognito.d.ts.map +1 -0
- package/dist/types/provider/discord.d.ts +38 -0
- package/dist/types/provider/discord.d.ts.map +1 -0
- package/dist/types/provider/facebook.d.ts +74 -0
- package/dist/types/provider/facebook.d.ts.map +1 -0
- package/dist/types/provider/github.d.ts +38 -0
- package/dist/types/provider/github.d.ts.map +1 -0
- package/dist/types/provider/google.d.ts +74 -0
- package/dist/types/provider/google.d.ts.map +1 -0
- package/dist/types/provider/index.d.ts +4 -0
- package/dist/types/provider/index.d.ts.map +1 -0
- package/dist/types/provider/jumpcloud.d.ts +38 -0
- package/dist/types/provider/jumpcloud.d.ts.map +1 -0
- package/dist/types/provider/keycloak.d.ts +67 -0
- package/dist/types/provider/keycloak.d.ts.map +1 -0
- package/dist/types/provider/linkedin.d.ts +6 -0
- package/dist/types/provider/linkedin.d.ts.map +1 -0
- package/dist/types/provider/m2m.d.ts +34 -0
- package/dist/types/provider/m2m.d.ts.map +1 -0
- package/dist/types/provider/microsoft.d.ts +89 -0
- package/dist/types/provider/microsoft.d.ts.map +1 -0
- package/dist/types/provider/oauth2.d.ts +133 -0
- package/dist/types/provider/oauth2.d.ts.map +1 -0
- package/dist/types/provider/oidc.d.ts +91 -0
- package/dist/types/provider/oidc.d.ts.map +1 -0
- package/dist/types/provider/passkey.d.ts +143 -0
- package/dist/types/provider/passkey.d.ts.map +1 -0
- package/dist/types/provider/password.d.ts +210 -0
- package/dist/types/provider/password.d.ts.map +1 -0
- package/dist/types/provider/provider.d.ts +29 -0
- package/dist/types/provider/provider.d.ts.map +1 -0
- package/dist/types/provider/slack.d.ts +59 -0
- package/dist/types/provider/slack.d.ts.map +1 -0
- package/dist/types/provider/spotify.d.ts +38 -0
- package/dist/types/provider/spotify.d.ts.map +1 -0
- package/dist/types/provider/twitch.d.ts +38 -0
- package/dist/types/provider/twitch.d.ts.map +1 -0
- package/dist/types/provider/x.d.ts +38 -0
- package/dist/types/provider/x.d.ts.map +1 -0
- package/dist/types/provider/yahoo.d.ts +38 -0
- package/dist/types/provider/yahoo.d.ts.map +1 -0
- package/dist/types/random.d.ts +3 -0
- package/dist/types/random.d.ts.map +1 -0
- package/dist/types/storage/aws.d.ts +4 -0
- package/dist/types/storage/aws.d.ts.map +1 -0
- package/dist/types/storage/cloudflare.d.ts +34 -0
- package/dist/types/storage/cloudflare.d.ts.map +1 -0
- package/dist/types/storage/dynamo.d.ts +65 -0
- package/dist/types/storage/dynamo.d.ts.map +1 -0
- package/dist/types/storage/memory.d.ts +49 -0
- package/dist/types/storage/memory.d.ts.map +1 -0
- package/dist/types/storage/storage.d.ts +15 -0
- package/dist/types/storage/storage.d.ts.map +1 -0
- package/dist/types/subject.d.ts +122 -0
- package/dist/types/subject.d.ts.map +1 -0
- package/dist/types/ui/base.d.ts +5 -0
- package/dist/types/ui/base.d.ts.map +1 -0
- package/dist/types/ui/code.d.ts +104 -0
- package/dist/types/ui/code.d.ts.map +1 -0
- package/dist/types/ui/form.d.ts +6 -0
- package/dist/types/ui/form.d.ts.map +1 -0
- package/dist/types/ui/icon.d.ts +6 -0
- package/dist/types/ui/icon.d.ts.map +1 -0
- package/dist/types/ui/passkey.d.ts +5 -0
- package/dist/types/ui/passkey.d.ts.map +1 -0
- package/dist/types/ui/password.d.ts +139 -0
- package/dist/types/ui/password.d.ts.map +1 -0
- package/dist/types/ui/select.d.ts +55 -0
- package/dist/types/ui/select.d.ts.map +1 -0
- package/dist/types/ui/theme.d.ts +207 -0
- package/dist/types/ui/theme.d.ts.map +1 -0
- package/dist/types/util.d.ts +8 -0
- package/dist/types/util.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/client.ts +749 -0
- package/src/css.d.ts +4 -0
- package/src/error.ts +120 -0
- package/src/index.ts +26 -0
- package/src/issuer.ts +1302 -0
- package/src/jwt.ts +17 -0
- package/src/keys.ts +139 -0
- package/src/pkce.ts +40 -0
- package/src/provider/apple.ts +127 -0
- package/src/provider/arctic.ts +66 -0
- package/src/provider/code.ts +227 -0
- package/src/provider/cognito.ts +74 -0
- package/src/provider/discord.ts +45 -0
- package/src/provider/facebook.ts +84 -0
- package/src/provider/github.ts +45 -0
- package/src/provider/google.ts +85 -0
- package/src/provider/index.ts +3 -0
- package/src/provider/jumpcloud.ts +45 -0
- package/src/provider/keycloak.ts +75 -0
- package/src/provider/linkedin.ts +12 -0
- package/src/provider/m2m.ts +56 -0
- package/src/provider/microsoft.ts +100 -0
- package/src/provider/oauth2.ts +297 -0
- package/src/provider/oidc.ts +179 -0
- package/src/provider/passkey.ts +655 -0
- package/src/provider/password.ts +672 -0
- package/src/provider/provider.ts +33 -0
- package/src/provider/slack.ts +67 -0
- package/src/provider/spotify.ts +45 -0
- package/src/provider/twitch.ts +45 -0
- package/src/provider/x.ts +46 -0
- package/src/provider/yahoo.ts +45 -0
- package/src/random.ts +24 -0
- package/src/storage/aws.ts +59 -0
- package/src/storage/cloudflare.ts +77 -0
- package/src/storage/dynamo.ts +193 -0
- package/src/storage/memory.ts +135 -0
- package/src/storage/storage.ts +46 -0
- package/src/subject.ts +130 -0
- package/src/ui/base.tsx +118 -0
- package/src/ui/code.tsx +215 -0
- package/src/ui/form.tsx +40 -0
- package/src/ui/icon.tsx +95 -0
- package/src/ui/passkey.tsx +321 -0
- package/src/ui/password.tsx +405 -0
- package/src/ui/select.tsx +221 -0
- package/src/ui/theme.ts +319 -0
- package/src/ui/ui.css +252 -0
- package/src/util.ts +58 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use the OpenAuth client kick off your OAuth flows, exchange tokens, refresh tokens,
|
|
3
|
+
* and verify tokens.
|
|
4
|
+
*
|
|
5
|
+
* First, create a client.
|
|
6
|
+
*
|
|
7
|
+
* ```ts title="client.ts"
|
|
8
|
+
* import { createClient } from "@openauthjs/openauth/client"
|
|
9
|
+
*
|
|
10
|
+
* const client = createClient({
|
|
11
|
+
* clientID: "my-client",
|
|
12
|
+
* issuer: "https://auth.myserver.com"
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Kick off the OAuth flow by calling `authorize`.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const redirect_uri = "https://myserver.com/callback"
|
|
20
|
+
*
|
|
21
|
+
* const { url } = await client.authorize(
|
|
22
|
+
* redirect_uri,
|
|
23
|
+
* "code"
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* When the user completes the flow, `exchange` the code for tokens.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const tokens = await client.exchange(query.get("code"), redirect_uri)
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* And `verify` the tokens.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const verified = await client.verify(subjects, tokens.access)
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @packageDocumentation
|
|
40
|
+
*/
|
|
41
|
+
import {
|
|
42
|
+
createLocalJWKSet,
|
|
43
|
+
errors,
|
|
44
|
+
JSONWebKeySet,
|
|
45
|
+
jwtVerify,
|
|
46
|
+
decodeJwt,
|
|
47
|
+
} from "jose"
|
|
48
|
+
import { SubjectSchema } from "./subject.js"
|
|
49
|
+
import type { v1 } from "@standard-schema/spec"
|
|
50
|
+
import {
|
|
51
|
+
InvalidAccessTokenError,
|
|
52
|
+
InvalidAuthorizationCodeError,
|
|
53
|
+
InvalidRefreshTokenError,
|
|
54
|
+
InvalidSubjectError,
|
|
55
|
+
} from "./error.js"
|
|
56
|
+
import { generatePKCE } from "./pkce.js"
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The well-known information for an OAuth 2.0 authorization server.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export interface WellKnown {
|
|
63
|
+
/**
|
|
64
|
+
* The URI to the JWKS endpoint.
|
|
65
|
+
*/
|
|
66
|
+
jwks_uri: string
|
|
67
|
+
/**
|
|
68
|
+
* The URI to the token endpoint.
|
|
69
|
+
*/
|
|
70
|
+
token_endpoint: string
|
|
71
|
+
/**
|
|
72
|
+
* The URI to the authorization endpoint.
|
|
73
|
+
*/
|
|
74
|
+
authorization_endpoint: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The tokens returned by the auth server.
|
|
79
|
+
*/
|
|
80
|
+
export interface Tokens {
|
|
81
|
+
/**
|
|
82
|
+
* The access token.
|
|
83
|
+
*/
|
|
84
|
+
access: string
|
|
85
|
+
/**
|
|
86
|
+
* The refresh token.
|
|
87
|
+
*/
|
|
88
|
+
refresh: string
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The number of seconds until the access token expires.
|
|
92
|
+
*/
|
|
93
|
+
expiresIn: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ResponseLike {
|
|
97
|
+
json(): Promise<unknown>
|
|
98
|
+
ok: Response["ok"]
|
|
99
|
+
}
|
|
100
|
+
type FetchLike = (...args: any[]) => Promise<ResponseLike>
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The challenge that you can use to verify the code.
|
|
104
|
+
*/
|
|
105
|
+
export type Challenge = {
|
|
106
|
+
/**
|
|
107
|
+
* The state that was sent to the redirect URI.
|
|
108
|
+
*/
|
|
109
|
+
state: string
|
|
110
|
+
/**
|
|
111
|
+
* The verifier that was sent to the redirect URI.
|
|
112
|
+
*/
|
|
113
|
+
verifier?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Configure the client.
|
|
118
|
+
*/
|
|
119
|
+
export interface ClientInput {
|
|
120
|
+
/**
|
|
121
|
+
* The client ID. This is just a string to identify your app.
|
|
122
|
+
*
|
|
123
|
+
* If you have a web app and a mobile app, you want to use different client IDs both.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* {
|
|
128
|
+
* clientID: "my-client"
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
clientID: string
|
|
133
|
+
/**
|
|
134
|
+
* The URL of your OpenAuth server.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* {
|
|
139
|
+
* issuer: "https://auth.myserver.com"
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
issuer?: string
|
|
144
|
+
/**
|
|
145
|
+
* Optionally, override the internally used fetch function.
|
|
146
|
+
*
|
|
147
|
+
* This is useful if you are using a polyfilled fetch function in your application and you
|
|
148
|
+
* want the client to use it too.
|
|
149
|
+
*/
|
|
150
|
+
fetch?: FetchLike
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface AuthorizeOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Enable the PKCE flow. This is for SPA apps.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* {
|
|
159
|
+
* pkce: true
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @default false
|
|
164
|
+
*/
|
|
165
|
+
pkce?: boolean
|
|
166
|
+
/**
|
|
167
|
+
* The provider you want to use for the OAuth flow.
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* {
|
|
171
|
+
* provider: "google"
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* If no provider is specified, the user is directed to a page where they can select from the
|
|
176
|
+
* list of configured providers.
|
|
177
|
+
*
|
|
178
|
+
* If there's only one provider configured, the user will be redirected to that.
|
|
179
|
+
*/
|
|
180
|
+
provider?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface AuthorizeResult {
|
|
184
|
+
/**
|
|
185
|
+
* The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps.
|
|
186
|
+
*
|
|
187
|
+
* This is an object that you _stringify_ and store it in session storage.
|
|
188
|
+
*
|
|
189
|
+
* ```ts
|
|
190
|
+
* sessionStorage.setItem("challenge", JSON.stringify(challenge))
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
challenge: Challenge
|
|
194
|
+
/**
|
|
195
|
+
* The URL to redirect the user to. This starts the OAuth flow.
|
|
196
|
+
*
|
|
197
|
+
* For example, for SPA apps.
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* location.href = url
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
url: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returned when the exchange is successful.
|
|
208
|
+
*/
|
|
209
|
+
export interface ExchangeSuccess {
|
|
210
|
+
/**
|
|
211
|
+
* This is always `false` when the exchange is successful.
|
|
212
|
+
*/
|
|
213
|
+
err: false
|
|
214
|
+
/**
|
|
215
|
+
* The access and refresh tokens.
|
|
216
|
+
*/
|
|
217
|
+
tokens: Tokens
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returned when the exchange fails.
|
|
222
|
+
*/
|
|
223
|
+
export interface ExchangeError {
|
|
224
|
+
/**
|
|
225
|
+
* The type of error that occurred. You can handle this by checking the type.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
|
|
230
|
+
*
|
|
231
|
+
* console.log(err instanceof InvalidAuthorizationCodeError)
|
|
232
|
+
*```
|
|
233
|
+
*/
|
|
234
|
+
err: InvalidAuthorizationCodeError
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface RefreshOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Optionally, pass in the access token.
|
|
240
|
+
*/
|
|
241
|
+
access?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returned when the refresh is successful.
|
|
246
|
+
*/
|
|
247
|
+
export interface RefreshSuccess {
|
|
248
|
+
/**
|
|
249
|
+
* This is always `false` when the refresh is successful.
|
|
250
|
+
*/
|
|
251
|
+
err: false
|
|
252
|
+
/**
|
|
253
|
+
* Returns the refreshed tokens only if they've been refreshed.
|
|
254
|
+
*
|
|
255
|
+
* If they are still valid, this will be `undefined`.
|
|
256
|
+
*/
|
|
257
|
+
tokens?: Tokens
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Returned when the refresh fails.
|
|
262
|
+
*/
|
|
263
|
+
export interface RefreshError {
|
|
264
|
+
/**
|
|
265
|
+
* The type of error that occurred. You can handle this by checking the type.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
|
|
270
|
+
*
|
|
271
|
+
* console.log(err instanceof InvalidRefreshTokenError)
|
|
272
|
+
*```
|
|
273
|
+
*/
|
|
274
|
+
err: InvalidRefreshTokenError | InvalidAccessTokenError
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface VerifyOptions {
|
|
278
|
+
/**
|
|
279
|
+
* Optionally, pass in the refresh token.
|
|
280
|
+
*
|
|
281
|
+
* If passed in, this will automatically refresh the access token if it has expired.
|
|
282
|
+
*/
|
|
283
|
+
refresh?: string
|
|
284
|
+
/**
|
|
285
|
+
* @internal
|
|
286
|
+
*/
|
|
287
|
+
issuer?: string
|
|
288
|
+
/**
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
audience?: string
|
|
292
|
+
/**
|
|
293
|
+
* Optionally, override the internally used fetch function.
|
|
294
|
+
*
|
|
295
|
+
* This is useful if you are using a polyfilled fetch function in your application and you
|
|
296
|
+
* want the client to use it too.
|
|
297
|
+
*/
|
|
298
|
+
fetch?: FetchLike
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface VerifyResult<T extends SubjectSchema> {
|
|
302
|
+
/**
|
|
303
|
+
* This is always `undefined` when the verify is successful.
|
|
304
|
+
*/
|
|
305
|
+
err?: undefined
|
|
306
|
+
/**
|
|
307
|
+
* Returns the refreshed tokens only if they’ve been refreshed.
|
|
308
|
+
*
|
|
309
|
+
* If they are still valid, this will be undefined.
|
|
310
|
+
*/
|
|
311
|
+
tokens?: Tokens
|
|
312
|
+
/**
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
aud: string
|
|
316
|
+
/**
|
|
317
|
+
* The decoded subjects from the access token.
|
|
318
|
+
*
|
|
319
|
+
* Has the same shape as the subjects you defined when creating the issuer.
|
|
320
|
+
*/
|
|
321
|
+
subject: {
|
|
322
|
+
[type in keyof T]: { type: type; properties: v1.InferOutput<T[type]> }
|
|
323
|
+
}[keyof T]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Returned when the verify call fails.
|
|
328
|
+
*/
|
|
329
|
+
export interface VerifyError {
|
|
330
|
+
/**
|
|
331
|
+
* The type of error that occurred. You can handle this by checking the type.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
|
|
336
|
+
*
|
|
337
|
+
* console.log(err instanceof InvalidRefreshTokenError)
|
|
338
|
+
*```
|
|
339
|
+
*/
|
|
340
|
+
err: InvalidRefreshTokenError | InvalidAccessTokenError
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* An instance of the OpenAuth client contains the following methods.
|
|
345
|
+
*/
|
|
346
|
+
export interface Client {
|
|
347
|
+
/**
|
|
348
|
+
* Start the autorization flow. For example, in SSR sites.
|
|
349
|
+
*
|
|
350
|
+
* ```ts
|
|
351
|
+
* const { url } = await client.authorize(<redirect_uri>, "code")
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* This takes a redirect URI and the type of flow you want to use. The redirect URI is the
|
|
355
|
+
* location where the user will be redirected to after the flow is complete.
|
|
356
|
+
*
|
|
357
|
+
* Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more
|
|
358
|
+
* secure.
|
|
359
|
+
*
|
|
360
|
+
* :::tip
|
|
361
|
+
* This returns a URL to redirect the user to. This starts the OAuth flow.
|
|
362
|
+
* :::
|
|
363
|
+
*
|
|
364
|
+
* This returns a URL to the auth server. You can redirect the user to the URL to start the
|
|
365
|
+
* OAuth flow.
|
|
366
|
+
*
|
|
367
|
+
* For SPA apps, we recommend using the PKCE flow.
|
|
368
|
+
*
|
|
369
|
+
* ```ts {4}
|
|
370
|
+
* const { challenge, url } = await client.authorize(
|
|
371
|
+
* <redirect_uri>,
|
|
372
|
+
* "code",
|
|
373
|
+
* { pkce: true }
|
|
374
|
+
* )
|
|
375
|
+
* ```
|
|
376
|
+
*
|
|
377
|
+
* This returns a redirect URL and a challenge that you need to use later to verify the code.
|
|
378
|
+
*/
|
|
379
|
+
authorize(
|
|
380
|
+
redirectURI: string,
|
|
381
|
+
response: "code" | "token",
|
|
382
|
+
opts?: AuthorizeOptions,
|
|
383
|
+
): Promise<AuthorizeResult>
|
|
384
|
+
/**
|
|
385
|
+
* Exchange the code for access and refresh tokens.
|
|
386
|
+
*
|
|
387
|
+
* ```ts
|
|
388
|
+
* const exchanged = await client.exchange(<code>, <redirect_uri>)
|
|
389
|
+
* ```
|
|
390
|
+
*
|
|
391
|
+
* You call this after the user has been redirected back to your app after the OAuth flow.
|
|
392
|
+
*
|
|
393
|
+
* :::tip
|
|
394
|
+
* For SSR sites, the code is returned in the query parameter.
|
|
395
|
+
* :::
|
|
396
|
+
*
|
|
397
|
+
* So the code comes from the query parameter in the redirect URI. The redirect URI here is
|
|
398
|
+
* the one that you passed in to the `authorize` call when starting the flow.
|
|
399
|
+
*
|
|
400
|
+
* :::tip
|
|
401
|
+
* For SPA sites, the code is returned through the URL hash.
|
|
402
|
+
* :::
|
|
403
|
+
*
|
|
404
|
+
* If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL
|
|
405
|
+
* hash.
|
|
406
|
+
*
|
|
407
|
+
* ```ts {4}
|
|
408
|
+
* const exchanged = await client.exchange(
|
|
409
|
+
* <code>,
|
|
410
|
+
* <redirect_uri>,
|
|
411
|
+
* <challenge.verifier>
|
|
412
|
+
* )
|
|
413
|
+
* ```
|
|
414
|
+
*
|
|
415
|
+
* You also need to pass in the previously stored challenge verifier.
|
|
416
|
+
*
|
|
417
|
+
* This method returns the access and refresh tokens. Or if it fails, it returns an error that
|
|
418
|
+
* you can handle depending on the error.
|
|
419
|
+
*
|
|
420
|
+
* ```ts
|
|
421
|
+
* import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
|
|
422
|
+
*
|
|
423
|
+
* if (exchanged.err) {
|
|
424
|
+
* if (exchanged.err instanceof InvalidAuthorizationCodeError) {
|
|
425
|
+
* // handle invalid code error
|
|
426
|
+
* }
|
|
427
|
+
* else {
|
|
428
|
+
* // handle other errors
|
|
429
|
+
* }
|
|
430
|
+
* }
|
|
431
|
+
*
|
|
432
|
+
* const { access, refresh } = exchanged.tokens
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
exchange(
|
|
436
|
+
code: string,
|
|
437
|
+
redirectURI: string,
|
|
438
|
+
verifier?: string,
|
|
439
|
+
): Promise<ExchangeSuccess | ExchangeError>
|
|
440
|
+
/**
|
|
441
|
+
* Refreshes the tokens if they have expired. This is used in an SPA app to maintain the
|
|
442
|
+
* session, without logging the user out.
|
|
443
|
+
*
|
|
444
|
+
* ```ts
|
|
445
|
+
* const next = await client.refresh(<refresh_token>)
|
|
446
|
+
* ```
|
|
447
|
+
*
|
|
448
|
+
* Can optionally take the access token as well. If passed in, this will skip the refresh
|
|
449
|
+
* if the access token is still valid.
|
|
450
|
+
*
|
|
451
|
+
* ```ts
|
|
452
|
+
* const next = await client.refresh(<refresh_token>, { access: <access_token> })
|
|
453
|
+
* ```
|
|
454
|
+
*
|
|
455
|
+
* This returns the refreshed tokens only if they've been refreshed.
|
|
456
|
+
*
|
|
457
|
+
* ```ts
|
|
458
|
+
* if (!next.err) {
|
|
459
|
+
* // tokens are still valid
|
|
460
|
+
* }
|
|
461
|
+
* if (next.tokens) {
|
|
462
|
+
* const { access, refresh } = next.tokens
|
|
463
|
+
* }
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* Or if it fails, it returns an error that you can handle depending on the error.
|
|
467
|
+
*
|
|
468
|
+
* ```ts
|
|
469
|
+
* import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
|
|
470
|
+
*
|
|
471
|
+
* if (next.err) {
|
|
472
|
+
* if (next.err instanceof InvalidRefreshTokenError) {
|
|
473
|
+
* // handle invalid refresh token error
|
|
474
|
+
* }
|
|
475
|
+
* else {
|
|
476
|
+
* // handle other errors
|
|
477
|
+
* }
|
|
478
|
+
* }
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
refresh(
|
|
482
|
+
refresh: string,
|
|
483
|
+
opts?: RefreshOptions,
|
|
484
|
+
): Promise<RefreshSuccess | RefreshError>
|
|
485
|
+
/**
|
|
486
|
+
* Verify the token in the incoming request.
|
|
487
|
+
*
|
|
488
|
+
* This is typically used for SSR sites where the token is stored in an HTTP only cookie. And
|
|
489
|
+
* is passed to the server on every request.
|
|
490
|
+
*
|
|
491
|
+
* ```ts
|
|
492
|
+
* const verified = await client.verify(<subjects>, <token>)
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* This takes the subjects that you had previously defined when creating the issuer.
|
|
496
|
+
*
|
|
497
|
+
* :::tip
|
|
498
|
+
* If the refresh token is passed in, it'll automatically refresh the access token.
|
|
499
|
+
* :::
|
|
500
|
+
*
|
|
501
|
+
* This can optionally take the refresh token as well. If passed in, it'll automatically
|
|
502
|
+
* refresh the access token if it has expired.
|
|
503
|
+
*
|
|
504
|
+
* ```ts
|
|
505
|
+
* const verified = await client.verify(<subjects>, <token>, { refresh: <refresh_token> })
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* This returns the decoded subjects from the access token. And the tokens if they've been
|
|
509
|
+
* refreshed.
|
|
510
|
+
*
|
|
511
|
+
* ```ts
|
|
512
|
+
* // based on the subjects you defined earlier
|
|
513
|
+
* console.log(verified.subject.properties.userID)
|
|
514
|
+
*
|
|
515
|
+
* if (verified.tokens) {
|
|
516
|
+
* const { access, refresh } = verified.tokens
|
|
517
|
+
* }
|
|
518
|
+
* ```
|
|
519
|
+
*
|
|
520
|
+
* Or if it fails, it returns an error that you can handle depending on the error.
|
|
521
|
+
*
|
|
522
|
+
* ```ts
|
|
523
|
+
* import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
|
|
524
|
+
*
|
|
525
|
+
* if (verified.err) {
|
|
526
|
+
* if (verified.err instanceof InvalidRefreshTokenError) {
|
|
527
|
+
* // handle invalid refresh token error
|
|
528
|
+
* }
|
|
529
|
+
* else {
|
|
530
|
+
* // handle other errors
|
|
531
|
+
* }
|
|
532
|
+
* }
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
verify<T extends SubjectSchema>(
|
|
536
|
+
subjects: T,
|
|
537
|
+
token: string,
|
|
538
|
+
options?: VerifyOptions,
|
|
539
|
+
): Promise<VerifyResult<T> | VerifyError>
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Create an OpenAuth client.
|
|
544
|
+
*
|
|
545
|
+
* @param input - Configure the client.
|
|
546
|
+
*/
|
|
547
|
+
export function createClient(input: ClientInput): Client {
|
|
548
|
+
const jwksCache = new Map<string, ReturnType<typeof createLocalJWKSet>>()
|
|
549
|
+
const issuerCache = new Map<string, WellKnown>()
|
|
550
|
+
const issuer = input.issuer || process.env.OPENAUTH_ISSUER
|
|
551
|
+
if (!issuer) throw new Error("No issuer")
|
|
552
|
+
const f = input.fetch ?? fetch
|
|
553
|
+
|
|
554
|
+
async function getIssuer() {
|
|
555
|
+
const cached = issuerCache.get(issuer!)
|
|
556
|
+
if (cached) return cached
|
|
557
|
+
const wellKnown = (await (f || fetch)(
|
|
558
|
+
`${issuer}/.well-known/oauth-authorization-server`,
|
|
559
|
+
).then((r) => r.json())) as WellKnown
|
|
560
|
+
issuerCache.set(issuer!, wellKnown)
|
|
561
|
+
return wellKnown
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function getJWKS() {
|
|
565
|
+
const wk = await getIssuer()
|
|
566
|
+
const cached = jwksCache.get(issuer!)
|
|
567
|
+
if (cached) return cached
|
|
568
|
+
const keyset = (await (f || fetch)(wk.jwks_uri).then((r) =>
|
|
569
|
+
r.json(),
|
|
570
|
+
)) as JSONWebKeySet
|
|
571
|
+
const result = createLocalJWKSet(keyset)
|
|
572
|
+
jwksCache.set(issuer!, result)
|
|
573
|
+
return result
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const result = {
|
|
577
|
+
async authorize(
|
|
578
|
+
redirectURI: string,
|
|
579
|
+
response: "code" | "token",
|
|
580
|
+
opts?: AuthorizeOptions,
|
|
581
|
+
) {
|
|
582
|
+
const result = new URL(issuer + "/authorize")
|
|
583
|
+
const challenge: Challenge = {
|
|
584
|
+
state: crypto.randomUUID(),
|
|
585
|
+
}
|
|
586
|
+
result.searchParams.set("client_id", input.clientID)
|
|
587
|
+
result.searchParams.set("redirect_uri", redirectURI)
|
|
588
|
+
result.searchParams.set("response_type", response)
|
|
589
|
+
result.searchParams.set("state", challenge.state)
|
|
590
|
+
if (opts?.provider) result.searchParams.set("provider", opts.provider)
|
|
591
|
+
if (opts?.pkce && response === "code") {
|
|
592
|
+
const pkce = await generatePKCE()
|
|
593
|
+
result.searchParams.set("code_challenge_method", "S256")
|
|
594
|
+
result.searchParams.set("code_challenge", pkce.challenge)
|
|
595
|
+
challenge.verifier = pkce.verifier
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
challenge,
|
|
599
|
+
url: result.toString(),
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
/**
|
|
603
|
+
* @deprecated use `authorize` instead, it will do pkce by default unless disabled with `opts.pkce = false`
|
|
604
|
+
*/
|
|
605
|
+
async pkce(
|
|
606
|
+
redirectURI: string,
|
|
607
|
+
opts?: {
|
|
608
|
+
provider?: string
|
|
609
|
+
},
|
|
610
|
+
) {
|
|
611
|
+
const result = new URL(issuer + "/authorize")
|
|
612
|
+
if (opts?.provider) result.searchParams.set("provider", opts.provider)
|
|
613
|
+
result.searchParams.set("client_id", input.clientID)
|
|
614
|
+
result.searchParams.set("redirect_uri", redirectURI)
|
|
615
|
+
result.searchParams.set("response_type", "code")
|
|
616
|
+
const pkce = await generatePKCE()
|
|
617
|
+
result.searchParams.set("code_challenge_method", "S256")
|
|
618
|
+
result.searchParams.set("code_challenge", pkce.challenge)
|
|
619
|
+
return [pkce.verifier, result.toString()]
|
|
620
|
+
},
|
|
621
|
+
async exchange(
|
|
622
|
+
code: string,
|
|
623
|
+
redirectURI: string,
|
|
624
|
+
verifier?: string,
|
|
625
|
+
): Promise<ExchangeSuccess | ExchangeError> {
|
|
626
|
+
const tokens = await f(issuer + "/token", {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: {
|
|
629
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
630
|
+
},
|
|
631
|
+
body: new URLSearchParams({
|
|
632
|
+
code,
|
|
633
|
+
redirect_uri: redirectURI,
|
|
634
|
+
grant_type: "authorization_code",
|
|
635
|
+
client_id: input.clientID,
|
|
636
|
+
code_verifier: verifier || "",
|
|
637
|
+
}).toString(),
|
|
638
|
+
})
|
|
639
|
+
const json = (await tokens.json()) as any
|
|
640
|
+
if (!tokens.ok) {
|
|
641
|
+
return {
|
|
642
|
+
err: new InvalidAuthorizationCodeError(),
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
err: false,
|
|
647
|
+
tokens: {
|
|
648
|
+
access: json.access_token as string,
|
|
649
|
+
refresh: json.refresh_token as string,
|
|
650
|
+
expiresIn: json.expires_in as number,
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
async refresh(
|
|
655
|
+
refresh: string,
|
|
656
|
+
opts?: RefreshOptions,
|
|
657
|
+
): Promise<RefreshSuccess | RefreshError> {
|
|
658
|
+
if (opts && opts.access) {
|
|
659
|
+
const decoded = decodeJwt(opts.access)
|
|
660
|
+
if (!decoded) {
|
|
661
|
+
return {
|
|
662
|
+
err: new InvalidAccessTokenError(),
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// allow 30s window for expiration
|
|
666
|
+
if ((decoded.exp || 0) > Date.now() / 1000 + 30) {
|
|
667
|
+
return {
|
|
668
|
+
err: false,
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const tokens = await f(issuer + "/token", {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: {
|
|
675
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
676
|
+
},
|
|
677
|
+
body: new URLSearchParams({
|
|
678
|
+
grant_type: "refresh_token",
|
|
679
|
+
refresh_token: refresh,
|
|
680
|
+
}).toString(),
|
|
681
|
+
})
|
|
682
|
+
const json = (await tokens.json()) as any
|
|
683
|
+
if (!tokens.ok) {
|
|
684
|
+
return {
|
|
685
|
+
err: new InvalidRefreshTokenError(),
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
err: false,
|
|
690
|
+
tokens: {
|
|
691
|
+
access: json.access_token as string,
|
|
692
|
+
refresh: json.refresh_token as string,
|
|
693
|
+
expiresIn: json.expires_in as number,
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
async verify<T extends SubjectSchema>(
|
|
698
|
+
subjects: T,
|
|
699
|
+
token: string,
|
|
700
|
+
options?: VerifyOptions,
|
|
701
|
+
): Promise<VerifyResult<T> | VerifyError> {
|
|
702
|
+
const jwks = await getJWKS()
|
|
703
|
+
try {
|
|
704
|
+
const result = await jwtVerify<{
|
|
705
|
+
mode: "access"
|
|
706
|
+
type: keyof T
|
|
707
|
+
properties: v1.InferInput<T[keyof T]>
|
|
708
|
+
}>(token, jwks, {
|
|
709
|
+
issuer,
|
|
710
|
+
})
|
|
711
|
+
const validated = await subjects[result.payload.type][
|
|
712
|
+
"~standard"
|
|
713
|
+
].validate(result.payload.properties)
|
|
714
|
+
if (!validated.issues && result.payload.mode === "access")
|
|
715
|
+
return {
|
|
716
|
+
aud: result.payload.aud as string,
|
|
717
|
+
subject: {
|
|
718
|
+
type: result.payload.type,
|
|
719
|
+
properties: validated.value,
|
|
720
|
+
} as any,
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
err: new InvalidSubjectError(),
|
|
724
|
+
}
|
|
725
|
+
} catch (e) {
|
|
726
|
+
if (e instanceof errors.JWTExpired && options?.refresh) {
|
|
727
|
+
const refreshed = await this.refresh(options.refresh)
|
|
728
|
+
if (refreshed.err) return refreshed
|
|
729
|
+
const verified = await result.verify(
|
|
730
|
+
subjects,
|
|
731
|
+
refreshed.tokens!.access,
|
|
732
|
+
{
|
|
733
|
+
refresh: refreshed.tokens!.refresh,
|
|
734
|
+
issuer,
|
|
735
|
+
fetch: options?.fetch,
|
|
736
|
+
},
|
|
737
|
+
)
|
|
738
|
+
if (verified.err) return verified
|
|
739
|
+
verified.tokens = refreshed.tokens
|
|
740
|
+
return verified
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
err: new InvalidAccessTokenError(),
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
}
|
|
748
|
+
return result
|
|
749
|
+
}
|