@_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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use this to connect authentication providers that support OAuth 2.0.
|
|
3
|
+
*
|
|
4
|
+
* ```ts {5-12}
|
|
5
|
+
* import { Oauth2Provider } from "@openauthjs/openauth/provider/oauth2"
|
|
6
|
+
*
|
|
7
|
+
* export default issuer({
|
|
8
|
+
* providers: {
|
|
9
|
+
* oauth2: Oauth2Provider({
|
|
10
|
+
* clientID: "1234567890",
|
|
11
|
+
* clientSecret: "0987654321",
|
|
12
|
+
* endpoint: {
|
|
13
|
+
* authorization: "https://auth.myserver.com/authorize",
|
|
14
|
+
* token: "https://auth.myserver.com/token"
|
|
15
|
+
* }
|
|
16
|
+
* })
|
|
17
|
+
* }
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createRemoteJWKSet, jwtVerify } from "jose"
|
|
26
|
+
import { OauthError } from "../error.js"
|
|
27
|
+
import { generatePKCE } from "../pkce.js"
|
|
28
|
+
import { getRelativeUrl } from "../util.js"
|
|
29
|
+
import { Provider } from "./provider.js"
|
|
30
|
+
|
|
31
|
+
export interface Oauth2Config {
|
|
32
|
+
/**
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
type?: string
|
|
36
|
+
/**
|
|
37
|
+
* The client ID.
|
|
38
|
+
*
|
|
39
|
+
* This is just a string to identify your app.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* {
|
|
44
|
+
* clientID: "my-client"
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
clientID: string
|
|
49
|
+
/**
|
|
50
|
+
* The client secret.
|
|
51
|
+
*
|
|
52
|
+
* This is a private key that's used to authenticate your app. It should be kept secret.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* {
|
|
57
|
+
* clientSecret: "0987654321"
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
clientSecret: string
|
|
62
|
+
/**
|
|
63
|
+
* The URLs of the authorization and token endpoints.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* {
|
|
68
|
+
* endpoint: {
|
|
69
|
+
* authorization: "https://auth.myserver.com/authorize",
|
|
70
|
+
* token: "https://auth.myserver.com/token",
|
|
71
|
+
* jwks: "https://auth.myserver.com/auth/keys"
|
|
72
|
+
* }
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
endpoint: {
|
|
77
|
+
/**
|
|
78
|
+
* The URL of the authorization endpoint.
|
|
79
|
+
*/
|
|
80
|
+
authorization: string
|
|
81
|
+
/**
|
|
82
|
+
* The URL of the token endpoint.
|
|
83
|
+
*/
|
|
84
|
+
token: string
|
|
85
|
+
/**
|
|
86
|
+
* The URL of the JWKS endpoint.
|
|
87
|
+
*/
|
|
88
|
+
jwks?: string
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A list of OAuth scopes that you want to request.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* {
|
|
96
|
+
* scopes: ["email", "profile"]
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
scopes: string[]
|
|
101
|
+
/**
|
|
102
|
+
* Whether to use PKCE (Proof Key for Code Exchange) for the authorization code flow.
|
|
103
|
+
* Some providers like x.com require this.
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
pkce?: boolean
|
|
107
|
+
/**
|
|
108
|
+
* Any additional parameters that you want to pass to the authorization endpoint.
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* {
|
|
112
|
+
* query: {
|
|
113
|
+
* access_type: "offline",
|
|
114
|
+
* prompt: "consent"
|
|
115
|
+
* }
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
query?: Record<string, string>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
export type Oauth2WrappedConfig = Omit<Oauth2Config, "endpoint" | "name">
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
export interface Oauth2Token {
|
|
131
|
+
access: string
|
|
132
|
+
refresh: string
|
|
133
|
+
expiry: number
|
|
134
|
+
id?: Record<string, any>
|
|
135
|
+
raw: Record<string, any>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ProviderState {
|
|
139
|
+
state: string
|
|
140
|
+
redirect: string
|
|
141
|
+
codeVerifier?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function Oauth2Provider(
|
|
145
|
+
config: Oauth2Config,
|
|
146
|
+
): Provider<{ tokenset: Oauth2Token; clientID: string }> {
|
|
147
|
+
const query = config.query || {}
|
|
148
|
+
|
|
149
|
+
// Helper function to handle token exchange and response building
|
|
150
|
+
async function handleCallbackLogic(
|
|
151
|
+
c: any,
|
|
152
|
+
ctx: any,
|
|
153
|
+
provider: ProviderState,
|
|
154
|
+
code: string | undefined,
|
|
155
|
+
) {
|
|
156
|
+
if (!provider || !code) {
|
|
157
|
+
return c.redirect(getRelativeUrl(c, "./authorize"))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const body = new URLSearchParams({
|
|
161
|
+
client_id: config.clientID,
|
|
162
|
+
client_secret: config.clientSecret,
|
|
163
|
+
code,
|
|
164
|
+
grant_type: "authorization_code",
|
|
165
|
+
redirect_uri: provider.redirect,
|
|
166
|
+
...(provider.codeVerifier
|
|
167
|
+
? { code_verifier: provider.codeVerifier }
|
|
168
|
+
: {}),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const json: any = await fetch(config.endpoint.token, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
175
|
+
Accept: "application/json",
|
|
176
|
+
},
|
|
177
|
+
body: body.toString(),
|
|
178
|
+
}).then((r) => r.json())
|
|
179
|
+
|
|
180
|
+
if ("error" in json) {
|
|
181
|
+
throw new OauthError(json.error, json.error_description)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let idTokenPayload: Record<string, any> | null = null
|
|
185
|
+
if (config.endpoint.jwks) {
|
|
186
|
+
const jwksEndpoint = new URL(config.endpoint.jwks)
|
|
187
|
+
// @ts-expect-error bun/node mismatch
|
|
188
|
+
const jwks = createRemoteJWKSet(jwksEndpoint)
|
|
189
|
+
const { payload } = await jwtVerify(json.id_token, jwks, {
|
|
190
|
+
audience: config.clientID,
|
|
191
|
+
})
|
|
192
|
+
idTokenPayload = payload
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ctx.success(c, {
|
|
196
|
+
clientID: config.clientID,
|
|
197
|
+
tokenset: {
|
|
198
|
+
get access() {
|
|
199
|
+
return json.access_token
|
|
200
|
+
},
|
|
201
|
+
get refresh() {
|
|
202
|
+
return json.refresh_token
|
|
203
|
+
},
|
|
204
|
+
get expiry() {
|
|
205
|
+
return json.expires_in
|
|
206
|
+
},
|
|
207
|
+
get id() {
|
|
208
|
+
if (!idTokenPayload) return null
|
|
209
|
+
return idTokenPayload
|
|
210
|
+
},
|
|
211
|
+
get raw() {
|
|
212
|
+
return json
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
type: config.type || "oauth2",
|
|
220
|
+
init(routes, ctx) {
|
|
221
|
+
routes.get("/authorize", async (c) => {
|
|
222
|
+
const state = crypto.randomUUID()
|
|
223
|
+
const pkce = config.pkce ? await generatePKCE() : undefined
|
|
224
|
+
await ctx.set<ProviderState>(c, "provider", 60 * 10, {
|
|
225
|
+
state,
|
|
226
|
+
redirect: getRelativeUrl(c, "./callback"),
|
|
227
|
+
codeVerifier: pkce?.verifier,
|
|
228
|
+
})
|
|
229
|
+
const authorization = new URL(config.endpoint.authorization)
|
|
230
|
+
authorization.searchParams.set("client_id", config.clientID)
|
|
231
|
+
authorization.searchParams.set(
|
|
232
|
+
"redirect_uri",
|
|
233
|
+
getRelativeUrl(c, "./callback"),
|
|
234
|
+
)
|
|
235
|
+
authorization.searchParams.set("response_type", "code")
|
|
236
|
+
authorization.searchParams.set("state", state)
|
|
237
|
+
authorization.searchParams.set("scope", config.scopes.join(" "))
|
|
238
|
+
if (pkce) {
|
|
239
|
+
authorization.searchParams.set("code_challenge", pkce.challenge)
|
|
240
|
+
authorization.searchParams.set("code_challenge_method", pkce.method)
|
|
241
|
+
}
|
|
242
|
+
for (const [key, value] of Object.entries(query)) {
|
|
243
|
+
authorization.searchParams.set(key, value)
|
|
244
|
+
}
|
|
245
|
+
return c.redirect(authorization.toString())
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
routes.get("/callback", async (c) => {
|
|
249
|
+
const provider = (await ctx.get(c, "provider")) as ProviderState
|
|
250
|
+
const code = c.req.query("code")
|
|
251
|
+
const state = c.req.query("state")
|
|
252
|
+
const error = c.req.query("error")
|
|
253
|
+
|
|
254
|
+
if (error)
|
|
255
|
+
throw new OauthError(
|
|
256
|
+
error.toString() as any,
|
|
257
|
+
c.req.query("error_description")?.toString() || "",
|
|
258
|
+
)
|
|
259
|
+
if (
|
|
260
|
+
!provider ||
|
|
261
|
+
!code ||
|
|
262
|
+
(provider.state && state !== provider.state)
|
|
263
|
+
) {
|
|
264
|
+
return c.redirect(getRelativeUrl(c, "./authorize"))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return handleCallbackLogic(c, ctx, provider, code)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
routes.post("/callback", async (c) => {
|
|
271
|
+
const provider = (await ctx.get(c, "provider")) as ProviderState
|
|
272
|
+
|
|
273
|
+
// Handle form data from POST request
|
|
274
|
+
const formData = await c.req.formData()
|
|
275
|
+
const code = formData.get("code")?.toString()
|
|
276
|
+
const state = formData.get("state")?.toString()
|
|
277
|
+
const error = formData.get("error")?.toString()
|
|
278
|
+
|
|
279
|
+
if (error)
|
|
280
|
+
throw new OauthError(
|
|
281
|
+
error as any,
|
|
282
|
+
formData.get("error_description")?.toString() || "",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
!provider ||
|
|
287
|
+
!code ||
|
|
288
|
+
(provider.state && state !== provider.state)
|
|
289
|
+
) {
|
|
290
|
+
return c.redirect(getRelativeUrl(c, "./authorize"))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return handleCallbackLogic(c, ctx, provider, code)
|
|
294
|
+
})
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use this to connect authentication providers that support OIDC.
|
|
3
|
+
*
|
|
4
|
+
* ```ts {5-8}
|
|
5
|
+
* import { OidcProvider } from "@openauthjs/openauth/provider/oidc"
|
|
6
|
+
*
|
|
7
|
+
* export default issuer({
|
|
8
|
+
* providers: {
|
|
9
|
+
* oauth2: OidcProvider({
|
|
10
|
+
* clientId: "1234567890",
|
|
11
|
+
* issuer: "https://auth.myserver.com"
|
|
12
|
+
* })
|
|
13
|
+
* }
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createLocalJWKSet, JSONWebKeySet, jwtVerify } from "jose"
|
|
22
|
+
import { WellKnown } from "../client.js"
|
|
23
|
+
import { OauthError } from "../error.js"
|
|
24
|
+
import { Provider } from "./provider.js"
|
|
25
|
+
import { JWTPayload } from "hono/utils/jwt/types"
|
|
26
|
+
import { getRelativeUrl, lazy } from "../util.js"
|
|
27
|
+
|
|
28
|
+
export interface OidcConfig {
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
type?: string
|
|
33
|
+
/**
|
|
34
|
+
* The client ID.
|
|
35
|
+
*
|
|
36
|
+
* This is just a string to identify your app.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* {
|
|
41
|
+
* clientID: "my-client"
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
clientID: string
|
|
46
|
+
/**
|
|
47
|
+
* The URL of your authorization server.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* {
|
|
52
|
+
* issuer: "https://auth.myserver.com"
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
issuer: string
|
|
57
|
+
/**
|
|
58
|
+
* A list of OIDC scopes that you want to request.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* {
|
|
63
|
+
* scopes: ["openid", "profile", "email"]
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
scopes?: string[]
|
|
68
|
+
/**
|
|
69
|
+
* Any additional parameters that you want to pass to the authorization endpoint.
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* {
|
|
73
|
+
* query: {
|
|
74
|
+
* prompt: "consent"
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
query?: Record<string, string>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export type OidcWrappedConfig = Omit<OidcConfig, "issuer" | "name">
|
|
86
|
+
|
|
87
|
+
interface ProviderState {
|
|
88
|
+
state: string
|
|
89
|
+
nonce: string
|
|
90
|
+
redirect: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export interface IdTokenResponse {
|
|
97
|
+
idToken: string
|
|
98
|
+
claims: Record<string, any>
|
|
99
|
+
raw: Record<string, any>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function OidcProvider(
|
|
103
|
+
config: OidcConfig,
|
|
104
|
+
): Provider<{ id: JWTPayload; clientID: string }> {
|
|
105
|
+
const query = config.query || {}
|
|
106
|
+
const scopes = config.scopes || []
|
|
107
|
+
|
|
108
|
+
const wk = lazy(() =>
|
|
109
|
+
fetch(config.issuer + "/.well-known/openid-configuration").then(
|
|
110
|
+
async (r) => {
|
|
111
|
+
if (!r.ok) throw new Error(await r.text())
|
|
112
|
+
return r.json() as Promise<WellKnown>
|
|
113
|
+
},
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const jwks = lazy(() =>
|
|
118
|
+
wk()
|
|
119
|
+
.then((r) => r.jwks_uri)
|
|
120
|
+
.then(async (uri) => {
|
|
121
|
+
const r = await fetch(uri)
|
|
122
|
+
if (!r.ok) throw new Error(await r.text())
|
|
123
|
+
return createLocalJWKSet((await r.json()) as JSONWebKeySet)
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
type: config.type || "oidc",
|
|
129
|
+
init(routes, ctx) {
|
|
130
|
+
routes.get("/authorize", async (c) => {
|
|
131
|
+
const provider: ProviderState = {
|
|
132
|
+
state: crypto.randomUUID(),
|
|
133
|
+
nonce: crypto.randomUUID(),
|
|
134
|
+
redirect: getRelativeUrl(c, "./callback"),
|
|
135
|
+
}
|
|
136
|
+
await ctx.set(c, "provider", 60 * 10, provider)
|
|
137
|
+
const authorization = new URL(
|
|
138
|
+
await wk().then((r) => r.authorization_endpoint),
|
|
139
|
+
)
|
|
140
|
+
authorization.searchParams.set("client_id", config.clientID)
|
|
141
|
+
authorization.searchParams.set("response_type", "id_token")
|
|
142
|
+
authorization.searchParams.set("response_mode", "form_post")
|
|
143
|
+
authorization.searchParams.set("state", provider.state)
|
|
144
|
+
authorization.searchParams.set("nonce", provider.nonce)
|
|
145
|
+
authorization.searchParams.set("redirect_uri", provider.redirect)
|
|
146
|
+
authorization.searchParams.set("scope", ["openid", ...scopes].join(" "))
|
|
147
|
+
for (const [key, value] of Object.entries(query)) {
|
|
148
|
+
authorization.searchParams.set(key, value)
|
|
149
|
+
}
|
|
150
|
+
return c.redirect(authorization.toString())
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
routes.post("/callback", async (c) => {
|
|
154
|
+
const provider = await ctx.get<ProviderState>(c, "provider")
|
|
155
|
+
if (!provider) return c.redirect(getRelativeUrl(c, "./authorize"))
|
|
156
|
+
const body = await c.req.formData()
|
|
157
|
+
const error = body.get("error")
|
|
158
|
+
if (error)
|
|
159
|
+
throw new OauthError(
|
|
160
|
+
error.toString() as any,
|
|
161
|
+
body.get("error_description")?.toString() || "",
|
|
162
|
+
)
|
|
163
|
+
const idToken = body.get("id_token")
|
|
164
|
+
if (!idToken)
|
|
165
|
+
throw new OauthError("invalid_request", "Missing id_token")
|
|
166
|
+
const result = await jwtVerify(idToken.toString(), await jwks(), {
|
|
167
|
+
audience: config.clientID,
|
|
168
|
+
})
|
|
169
|
+
if (result.payload.nonce !== provider.nonce) {
|
|
170
|
+
throw new OauthError("invalid_request", "Invalid nonce")
|
|
171
|
+
}
|
|
172
|
+
return ctx.success(c, {
|
|
173
|
+
id: result.payload,
|
|
174
|
+
clientID: config.clientID,
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|