@_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/jwt.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { JWTPayload, jwtVerify, KeyLike, SignJWT } from "jose"
|
|
2
|
+
|
|
3
|
+
export namespace jwt {
|
|
4
|
+
export function create(
|
|
5
|
+
payload: JWTPayload,
|
|
6
|
+
algorithm: string,
|
|
7
|
+
privateKey: KeyLike,
|
|
8
|
+
) {
|
|
9
|
+
return new SignJWT(payload)
|
|
10
|
+
.setProtectedHeader({ alg: algorithm, typ: "JWT", kid: "sst" })
|
|
11
|
+
.sign(privateKey)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function verify<T>(token: string, publicKey: KeyLike) {
|
|
15
|
+
return jwtVerify<T>(token, publicKey)
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exportJWK,
|
|
3
|
+
exportPKCS8,
|
|
4
|
+
exportSPKI,
|
|
5
|
+
generateKeyPair,
|
|
6
|
+
importPKCS8,
|
|
7
|
+
importSPKI,
|
|
8
|
+
JWK,
|
|
9
|
+
KeyLike,
|
|
10
|
+
} from "jose"
|
|
11
|
+
import { Storage, StorageAdapter } from "./storage/storage.js"
|
|
12
|
+
|
|
13
|
+
const signingAlg = "ES256"
|
|
14
|
+
const encryptionAlg = "RSA-OAEP-512"
|
|
15
|
+
|
|
16
|
+
interface SerializedKeyPair {
|
|
17
|
+
id: string
|
|
18
|
+
publicKey: string
|
|
19
|
+
privateKey: string
|
|
20
|
+
created: number
|
|
21
|
+
alg: string
|
|
22
|
+
expired?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface KeyPair {
|
|
26
|
+
id: string
|
|
27
|
+
alg: string
|
|
28
|
+
public: KeyLike
|
|
29
|
+
private: KeyLike
|
|
30
|
+
created: Date
|
|
31
|
+
expired?: Date
|
|
32
|
+
jwk: JWK
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated use `signingKeys` instead
|
|
37
|
+
*/
|
|
38
|
+
export async function legacySigningKeys(
|
|
39
|
+
storage: StorageAdapter,
|
|
40
|
+
): Promise<KeyPair[]> {
|
|
41
|
+
const alg = "RS512"
|
|
42
|
+
const results = [] as KeyPair[]
|
|
43
|
+
const scanner = Storage.scan<SerializedKeyPair>(storage, ["oauth:key"])
|
|
44
|
+
for await (const [_key, value] of scanner) {
|
|
45
|
+
const publicKey = await importSPKI(value.publicKey, alg, {
|
|
46
|
+
extractable: true,
|
|
47
|
+
})
|
|
48
|
+
const privateKey = await importPKCS8(value.privateKey, alg)
|
|
49
|
+
const jwk = await exportJWK(publicKey)
|
|
50
|
+
jwk.kid = value.id
|
|
51
|
+
results.push({
|
|
52
|
+
id: value.id,
|
|
53
|
+
alg,
|
|
54
|
+
created: new Date(value.created),
|
|
55
|
+
public: publicKey,
|
|
56
|
+
private: privateKey,
|
|
57
|
+
expired: new Date(1735858114000),
|
|
58
|
+
jwk,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return results
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function signingKeys(storage: StorageAdapter): Promise<KeyPair[]> {
|
|
65
|
+
const results = [] as KeyPair[]
|
|
66
|
+
const scanner = Storage.scan<SerializedKeyPair>(storage, ["signing:key"])
|
|
67
|
+
for await (const [_key, value] of scanner) {
|
|
68
|
+
const publicKey = await importSPKI(value.publicKey, value.alg, {
|
|
69
|
+
extractable: true,
|
|
70
|
+
})
|
|
71
|
+
const privateKey = await importPKCS8(value.privateKey, value.alg)
|
|
72
|
+
const jwk = await exportJWK(publicKey)
|
|
73
|
+
jwk.kid = value.id
|
|
74
|
+
jwk.use = "sig"
|
|
75
|
+
results.push({
|
|
76
|
+
id: value.id,
|
|
77
|
+
alg: signingAlg,
|
|
78
|
+
created: new Date(value.created),
|
|
79
|
+
expired: value.expired ? new Date(value.expired) : undefined,
|
|
80
|
+
public: publicKey,
|
|
81
|
+
private: privateKey,
|
|
82
|
+
jwk,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
results.sort((a, b) => b.created.getTime() - a.created.getTime())
|
|
86
|
+
if (results.filter((item) => !item.expired).length) return results
|
|
87
|
+
|
|
88
|
+
const key = await generateKeyPair(signingAlg, {
|
|
89
|
+
extractable: true,
|
|
90
|
+
})
|
|
91
|
+
const serialized: SerializedKeyPair = {
|
|
92
|
+
id: crypto.randomUUID(),
|
|
93
|
+
publicKey: await exportSPKI(key.publicKey),
|
|
94
|
+
privateKey: await exportPKCS8(key.privateKey),
|
|
95
|
+
created: Date.now(),
|
|
96
|
+
alg: signingAlg,
|
|
97
|
+
}
|
|
98
|
+
await Storage.set(storage, ["signing:key", serialized.id], serialized)
|
|
99
|
+
return signingKeys(storage)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function encryptionKeys(
|
|
103
|
+
storage: StorageAdapter,
|
|
104
|
+
): Promise<KeyPair[]> {
|
|
105
|
+
const results = [] as KeyPair[]
|
|
106
|
+
const scanner = Storage.scan<SerializedKeyPair>(storage, ["encryption:key"])
|
|
107
|
+
for await (const [_key, value] of scanner) {
|
|
108
|
+
const publicKey = await importSPKI(value.publicKey, value.alg, {
|
|
109
|
+
extractable: true,
|
|
110
|
+
})
|
|
111
|
+
const privateKey = await importPKCS8(value.privateKey, value.alg)
|
|
112
|
+
const jwk = await exportJWK(publicKey)
|
|
113
|
+
jwk.kid = value.id
|
|
114
|
+
results.push({
|
|
115
|
+
id: value.id,
|
|
116
|
+
alg: encryptionAlg,
|
|
117
|
+
created: new Date(value.created),
|
|
118
|
+
expired: value.expired ? new Date(value.expired) : undefined,
|
|
119
|
+
public: publicKey,
|
|
120
|
+
private: privateKey,
|
|
121
|
+
jwk,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
results.sort((a, b) => b.created.getTime() - a.created.getTime())
|
|
125
|
+
if (results.filter((item) => !item.expired).length) return results
|
|
126
|
+
|
|
127
|
+
const key = await generateKeyPair(encryptionAlg, {
|
|
128
|
+
extractable: true,
|
|
129
|
+
})
|
|
130
|
+
const serialized: SerializedKeyPair = {
|
|
131
|
+
id: crypto.randomUUID(),
|
|
132
|
+
publicKey: await exportSPKI(key.publicKey),
|
|
133
|
+
privateKey: await exportPKCS8(key.privateKey),
|
|
134
|
+
created: Date.now(),
|
|
135
|
+
alg: encryptionAlg,
|
|
136
|
+
}
|
|
137
|
+
await Storage.set(storage, ["encryption:key", serialized.id], serialized)
|
|
138
|
+
return encryptionKeys(storage)
|
|
139
|
+
}
|
package/src/pkce.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { base64url } from "jose"
|
|
2
|
+
|
|
3
|
+
function generateVerifier(length: number): string {
|
|
4
|
+
const buffer = new Uint8Array(length)
|
|
5
|
+
crypto.getRandomValues(buffer)
|
|
6
|
+
return base64url.encode(buffer)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function generateChallenge(verifier: string, method: "S256" | "plain") {
|
|
10
|
+
if (method === "plain") return verifier
|
|
11
|
+
const encoder = new TextEncoder()
|
|
12
|
+
const data = encoder.encode(verifier)
|
|
13
|
+
const hash = await crypto.subtle.digest("SHA-256", data)
|
|
14
|
+
return base64url.encode(new Uint8Array(hash))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generatePKCE(length: number = 64) {
|
|
18
|
+
if (length < 43 || length > 128) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Code verifier length must be between 43 and 128 characters",
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
const verifier = generateVerifier(length)
|
|
24
|
+
const challenge = await generateChallenge(verifier, "S256")
|
|
25
|
+
return {
|
|
26
|
+
verifier,
|
|
27
|
+
challenge,
|
|
28
|
+
method: "S256",
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function validatePKCE(
|
|
33
|
+
verifier: string,
|
|
34
|
+
challenge: string,
|
|
35
|
+
method: "S256" | "plain" = "S256",
|
|
36
|
+
) {
|
|
37
|
+
const generatedChallenge = await generateChallenge(verifier, method)
|
|
38
|
+
// timing safe equals?
|
|
39
|
+
return generatedChallenge === challenge
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use this provider to authenticate with Apple. Supports both OAuth2 and OIDC.
|
|
3
|
+
*
|
|
4
|
+
* #### Using OAuth
|
|
5
|
+
*
|
|
6
|
+
* ```ts {5-8}
|
|
7
|
+
* import { AppleProvider } from "@openauthjs/openauth/provider/apple"
|
|
8
|
+
*
|
|
9
|
+
* export default issuer({
|
|
10
|
+
* providers: {
|
|
11
|
+
* apple: AppleProvider({
|
|
12
|
+
* clientID: "1234567890",
|
|
13
|
+
* clientSecret: "0987654321"
|
|
14
|
+
* })
|
|
15
|
+
* }
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* #### Using OAuth with form_post response mode
|
|
20
|
+
*
|
|
21
|
+
* When requesting name or email scopes from Apple, you must use form_post response mode:
|
|
22
|
+
*
|
|
23
|
+
* ```ts {5-9}
|
|
24
|
+
* import { AppleProvider } from "@openauthjs/openauth/provider/apple"
|
|
25
|
+
*
|
|
26
|
+
* export default issuer({
|
|
27
|
+
* providers: {
|
|
28
|
+
* apple: AppleProvider({
|
|
29
|
+
* clientID: "1234567890",
|
|
30
|
+
* clientSecret: "0987654321",
|
|
31
|
+
* responseMode: "form_post"
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* #### Using OIDC
|
|
38
|
+
*
|
|
39
|
+
* ```ts {5-7}
|
|
40
|
+
* import { AppleOidcProvider } from "@openauthjs/openauth/provider/apple"
|
|
41
|
+
*
|
|
42
|
+
* export default issuer({
|
|
43
|
+
* providers: {
|
|
44
|
+
* apple: AppleOidcProvider({
|
|
45
|
+
* clientID: "1234567890"
|
|
46
|
+
* })
|
|
47
|
+
* }
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @packageDocumentation
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js"
|
|
55
|
+
import { OidcProvider, OidcWrappedConfig } from "./oidc.js"
|
|
56
|
+
|
|
57
|
+
export interface AppleConfig extends Oauth2WrappedConfig {
|
|
58
|
+
/**
|
|
59
|
+
* The response mode to use for the authorization request.
|
|
60
|
+
* Apple requires 'form_post' response mode when requesting name or email scopes.
|
|
61
|
+
* @default "query"
|
|
62
|
+
*/
|
|
63
|
+
responseMode?: "query" | "form_post"
|
|
64
|
+
}
|
|
65
|
+
export interface AppleOidcConfig extends OidcWrappedConfig {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create an Apple OAuth2 provider.
|
|
69
|
+
*
|
|
70
|
+
* @param config - The config for the provider.
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* // Using default query response mode (GET callback)
|
|
74
|
+
* AppleProvider({
|
|
75
|
+
* clientID: "1234567890",
|
|
76
|
+
* clientSecret: "0987654321"
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* // Using form_post response mode (POST callback)
|
|
80
|
+
* // Required when requesting name or email scope
|
|
81
|
+
* AppleProvider({
|
|
82
|
+
* clientID: "1234567890",
|
|
83
|
+
* clientSecret: "0987654321",
|
|
84
|
+
* responseMode: "form_post",
|
|
85
|
+
* scopes: ["name", "email"]
|
|
86
|
+
* })
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function AppleProvider(config: AppleConfig) {
|
|
90
|
+
const { responseMode, ...restConfig } = config
|
|
91
|
+
const additionalQuery =
|
|
92
|
+
responseMode === "form_post"
|
|
93
|
+
? { response_mode: "form_post", ...config.query }
|
|
94
|
+
: config.query || {}
|
|
95
|
+
|
|
96
|
+
return Oauth2Provider({
|
|
97
|
+
...restConfig,
|
|
98
|
+
type: "apple" as const,
|
|
99
|
+
endpoint: {
|
|
100
|
+
authorization: "https://appleid.apple.com/auth/authorize",
|
|
101
|
+
token: "https://appleid.apple.com/auth/token",
|
|
102
|
+
jwks: "https://appleid.apple.com/auth/keys",
|
|
103
|
+
},
|
|
104
|
+
query: additionalQuery,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create an Apple OIDC provider.
|
|
110
|
+
*
|
|
111
|
+
* This is useful if you just want to verify the user's email address.
|
|
112
|
+
*
|
|
113
|
+
* @param config - The config for the provider.
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* AppleOidcProvider({
|
|
117
|
+
* clientID: "1234567890"
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function AppleOidcProvider(config: AppleOidcConfig) {
|
|
122
|
+
return OidcProvider({
|
|
123
|
+
...config,
|
|
124
|
+
type: "apple" as const,
|
|
125
|
+
issuer: "https://appleid.apple.com",
|
|
126
|
+
})
|
|
127
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { OAuth2Tokens } from "arctic"
|
|
2
|
+
import { Context } from "hono"
|
|
3
|
+
import { Provider } from "./provider.js"
|
|
4
|
+
import { OauthError } from "../error.js"
|
|
5
|
+
import { getRelativeUrl } from "../util.js"
|
|
6
|
+
|
|
7
|
+
export interface ArcticProviderOptions {
|
|
8
|
+
scopes: string[]
|
|
9
|
+
clientID: string
|
|
10
|
+
clientSecret: string
|
|
11
|
+
query?: Record<string, string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProviderState {
|
|
15
|
+
state: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ArcticProvider(
|
|
19
|
+
provider: new (
|
|
20
|
+
clientID: string,
|
|
21
|
+
clientSecret: string,
|
|
22
|
+
callback: string,
|
|
23
|
+
) => {
|
|
24
|
+
createAuthorizationURL(state: string, scopes: string[]): URL
|
|
25
|
+
validateAuthorizationCode(code: string): Promise<OAuth2Tokens>
|
|
26
|
+
refreshAccessToken(refreshToken: string): Promise<OAuth2Tokens>
|
|
27
|
+
},
|
|
28
|
+
config: ArcticProviderOptions,
|
|
29
|
+
): Provider<{
|
|
30
|
+
tokenset: OAuth2Tokens
|
|
31
|
+
}> {
|
|
32
|
+
function getClient(c: Context) {
|
|
33
|
+
const callback = new URL(c.req.url)
|
|
34
|
+
const pathname = callback.pathname.replace(/authorize.*$/, "callback")
|
|
35
|
+
const url = getRelativeUrl(c, pathname)
|
|
36
|
+
return new provider(config.clientID, config.clientSecret, url)
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type: "arctic",
|
|
40
|
+
init(routes, ctx) {
|
|
41
|
+
routes.get("/authorize", async (c) => {
|
|
42
|
+
const client = getClient(c)
|
|
43
|
+
const state = crypto.randomUUID()
|
|
44
|
+
await ctx.set(c, "provider", 60 * 10, {
|
|
45
|
+
state,
|
|
46
|
+
})
|
|
47
|
+
return c.redirect(client.createAuthorizationURL(state, config.scopes))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
routes.get("/callback", async (c) => {
|
|
51
|
+
const client = getClient(c)
|
|
52
|
+
const provider = (await ctx.get(c, "provider")) as ProviderState
|
|
53
|
+
if (!provider) return c.redirect("../authorize")
|
|
54
|
+
const code = c.req.query("code")
|
|
55
|
+
const state = c.req.query("state")
|
|
56
|
+
if (!code) throw new Error("Missing code")
|
|
57
|
+
if (state !== provider.state)
|
|
58
|
+
throw new OauthError("invalid_request", "Invalid state")
|
|
59
|
+
const tokens = await client.validateAuthorizationCode(code)
|
|
60
|
+
return ctx.success(c, {
|
|
61
|
+
tokenset: tokens,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configures a provider that supports pin code authentication. This is usually paired with the
|
|
3
|
+
* `CodeUI`.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { CodeUI } from "@openauthjs/openauth/ui/code"
|
|
7
|
+
* import { CodeProvider } from "@openauthjs/openauth/provider/code"
|
|
8
|
+
*
|
|
9
|
+
* export default issuer({
|
|
10
|
+
* providers: {
|
|
11
|
+
* code: CodeProvider(
|
|
12
|
+
* CodeUI({
|
|
13
|
+
* copy: {
|
|
14
|
+
* code_info: "We'll send a pin code to your email"
|
|
15
|
+
* },
|
|
16
|
+
* sendCode: (claims, code) => console.log(claims.email, code)
|
|
17
|
+
* })
|
|
18
|
+
* )
|
|
19
|
+
* },
|
|
20
|
+
* // ...
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* You can customize the provider using.
|
|
25
|
+
*
|
|
26
|
+
* ```ts {7-9}
|
|
27
|
+
* const ui = CodeUI({
|
|
28
|
+
* // ...
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* export default issuer({
|
|
32
|
+
* providers: {
|
|
33
|
+
* code: CodeProvider(
|
|
34
|
+
* { ...ui, length: 4 }
|
|
35
|
+
* )
|
|
36
|
+
* },
|
|
37
|
+
* // ...
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Behind the scenes, the `CodeProvider` expects callbacks that implements request handlers
|
|
42
|
+
* that generate the UI for the following.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* CodeProvider({
|
|
46
|
+
* // ...
|
|
47
|
+
* request: (req, state, form, error) => Promise<Response>
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* This allows you to create your own UI.
|
|
52
|
+
*
|
|
53
|
+
* @packageDocumentation
|
|
54
|
+
*/
|
|
55
|
+
import { Context } from "hono"
|
|
56
|
+
import { Provider } from "./provider.js"
|
|
57
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random.js"
|
|
58
|
+
|
|
59
|
+
export interface CodeProviderConfig<
|
|
60
|
+
Claims extends Record<string, string> = Record<string, string>,
|
|
61
|
+
> {
|
|
62
|
+
/**
|
|
63
|
+
* The length of the pin code.
|
|
64
|
+
*
|
|
65
|
+
* @default 6
|
|
66
|
+
*/
|
|
67
|
+
length?: number
|
|
68
|
+
/**
|
|
69
|
+
* The request handler to generate the UI for the code flow.
|
|
70
|
+
*
|
|
71
|
+
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
|
72
|
+
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
|
73
|
+
* ojects.
|
|
74
|
+
*
|
|
75
|
+
* Also passes in the current `state` of the flow and any `error` that occurred.
|
|
76
|
+
*
|
|
77
|
+
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
|
78
|
+
* in return.
|
|
79
|
+
*/
|
|
80
|
+
request: (
|
|
81
|
+
req: Request,
|
|
82
|
+
state: CodeProviderState,
|
|
83
|
+
form?: FormData,
|
|
84
|
+
error?: CodeProviderError,
|
|
85
|
+
) => Promise<Response>
|
|
86
|
+
/**
|
|
87
|
+
* Callback to send the pin code to the user.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* {
|
|
92
|
+
* sendCode: async (claims, code) => {
|
|
93
|
+
* // Send the code through the email or phone number based on the claims
|
|
94
|
+
* }
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
sendCode: (claims: Claims, code: string) => Promise<void | CodeProviderError>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The state of the code flow.
|
|
103
|
+
*
|
|
104
|
+
* | State | Description |
|
|
105
|
+
* | ----- | ----------- |
|
|
106
|
+
* | `start` | The user is asked to enter their email address or phone number to start the flow. |
|
|
107
|
+
* | `code` | The user needs to enter the pin code to verify their _claim_. |
|
|
108
|
+
*/
|
|
109
|
+
export type CodeProviderState =
|
|
110
|
+
| {
|
|
111
|
+
type: "start"
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
type: "code"
|
|
115
|
+
resend?: boolean
|
|
116
|
+
code: string
|
|
117
|
+
claims: Record<string, string>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The errors that can happen on the code flow.
|
|
122
|
+
*
|
|
123
|
+
* | Error | Description |
|
|
124
|
+
* | ----- | ----------- |
|
|
125
|
+
* | `invalid_code` | The code is invalid. |
|
|
126
|
+
* | `invalid_claim` | The _claim_, email or phone number, is invalid. |
|
|
127
|
+
*/
|
|
128
|
+
export type CodeProviderError =
|
|
129
|
+
| {
|
|
130
|
+
type: "invalid_code"
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
type: "invalid_claim"
|
|
134
|
+
key: string
|
|
135
|
+
value: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function CodeProvider<
|
|
139
|
+
Claims extends Record<string, string> = Record<string, string>,
|
|
140
|
+
>(config: CodeProviderConfig<Claims>): Provider<{ claims: Claims }> {
|
|
141
|
+
const length = config.length || 6
|
|
142
|
+
function generate() {
|
|
143
|
+
return generateUnbiasedDigits(length)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: "code",
|
|
148
|
+
init(routes, ctx) {
|
|
149
|
+
async function transition(
|
|
150
|
+
c: Context,
|
|
151
|
+
next: CodeProviderState,
|
|
152
|
+
fd?: FormData,
|
|
153
|
+
err?: CodeProviderError,
|
|
154
|
+
) {
|
|
155
|
+
await ctx.set<CodeProviderState>(c, "provider", 60 * 60 * 24, next)
|
|
156
|
+
const resp = ctx.forward(
|
|
157
|
+
c,
|
|
158
|
+
await config.request(c.req.raw, next, fd, err),
|
|
159
|
+
)
|
|
160
|
+
return resp
|
|
161
|
+
}
|
|
162
|
+
routes.get("/authorize", async (c) => {
|
|
163
|
+
const resp = await transition(c, {
|
|
164
|
+
type: "start",
|
|
165
|
+
})
|
|
166
|
+
return resp
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
routes.post("/authorize", async (c) => {
|
|
170
|
+
const code = generate()
|
|
171
|
+
const fd = await c.req.formData()
|
|
172
|
+
const state = await ctx.get<CodeProviderState>(c, "provider")
|
|
173
|
+
const action = fd.get("action")?.toString()
|
|
174
|
+
|
|
175
|
+
if (action === "request" || action === "resend") {
|
|
176
|
+
const claims = Object.fromEntries(fd) as Claims
|
|
177
|
+
delete claims.action
|
|
178
|
+
const err = await config.sendCode(claims, code)
|
|
179
|
+
if (err) return transition(c, { type: "start" }, fd, err)
|
|
180
|
+
return transition(
|
|
181
|
+
c,
|
|
182
|
+
{
|
|
183
|
+
type: "code",
|
|
184
|
+
resend: action === "resend",
|
|
185
|
+
claims,
|
|
186
|
+
code,
|
|
187
|
+
},
|
|
188
|
+
fd,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
fd.get("action")?.toString() === "verify" &&
|
|
194
|
+
state.type === "code"
|
|
195
|
+
) {
|
|
196
|
+
const fd = await c.req.formData()
|
|
197
|
+
const compare = fd.get("code")?.toString()
|
|
198
|
+
if (
|
|
199
|
+
!state.code ||
|
|
200
|
+
!compare ||
|
|
201
|
+
!timingSafeCompare(state.code, compare)
|
|
202
|
+
) {
|
|
203
|
+
return transition(
|
|
204
|
+
c,
|
|
205
|
+
{
|
|
206
|
+
...state,
|
|
207
|
+
resend: false,
|
|
208
|
+
},
|
|
209
|
+
fd,
|
|
210
|
+
{ type: "invalid_code" },
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
await ctx.unset(c, "provider")
|
|
214
|
+
return ctx.forward(
|
|
215
|
+
c,
|
|
216
|
+
await ctx.success(c, { claims: state.claims as Claims }),
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @internal
|
|
226
|
+
*/
|
|
227
|
+
export type CodeProviderOptions = Parameters<typeof CodeProvider>[0]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use this provider to authenticate with a Cognito OAuth endpoint.
|
|
3
|
+
*
|
|
4
|
+
* ```ts {5-10}
|
|
5
|
+
* import { CognitoProvider } from "@openauthjs/openauth/provider/cognito"
|
|
6
|
+
*
|
|
7
|
+
* export default issuer({
|
|
8
|
+
* providers: {
|
|
9
|
+
* cognito: CognitoProvider({
|
|
10
|
+
* domain: "your-domain.auth.us-east-1.amazoncognito.com",
|
|
11
|
+
* region: "us-east-1",
|
|
12
|
+
* clientID: "1234567890",
|
|
13
|
+
* clientSecret: "0987654321"
|
|
14
|
+
* })
|
|
15
|
+
* }
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js"
|
|
23
|
+
|
|
24
|
+
export interface CognitoConfig extends Oauth2WrappedConfig {
|
|
25
|
+
/**
|
|
26
|
+
* The domain of the Cognito User Pool.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* {
|
|
31
|
+
* domain: "your-domain.auth.us-east-1.amazoncognito.com"
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
domain: string
|
|
36
|
+
/**
|
|
37
|
+
* The region the Cognito User Pool is in.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* {
|
|
42
|
+
* region: "us-east-1"
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
region: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a Cognito OAuth2 provider.
|
|
51
|
+
*
|
|
52
|
+
* @param config - The config for the provider.
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* CognitoProvider({
|
|
56
|
+
* domain: "your-domain.auth.us-east-1.amazoncognito.com",
|
|
57
|
+
* region: "us-east-1",
|
|
58
|
+
* clientID: "1234567890",
|
|
59
|
+
* clientSecret: "0987654321"
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function CognitoProvider(config: CognitoConfig) {
|
|
64
|
+
const domain = `${config.domain}.auth.${config.region}.amazoncognito.com`
|
|
65
|
+
|
|
66
|
+
return Oauth2Provider({
|
|
67
|
+
type: "cognito",
|
|
68
|
+
...config,
|
|
69
|
+
endpoint: {
|
|
70
|
+
authorization: `https://${domain}/oauth2/authorize`,
|
|
71
|
+
token: `https://${domain}/oauth2/token`,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|