@_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.
Files changed (192) hide show
  1. package/dist/esm/client.js +186 -0
  2. package/dist/esm/css.d.js +0 -0
  3. package/dist/esm/error.js +73 -0
  4. package/dist/esm/index.js +14 -0
  5. package/dist/esm/issuer.js +558 -0
  6. package/dist/esm/jwt.js +16 -0
  7. package/dist/esm/keys.js +113 -0
  8. package/dist/esm/pkce.js +35 -0
  9. package/dist/esm/provider/apple.js +28 -0
  10. package/dist/esm/provider/arctic.js +43 -0
  11. package/dist/esm/provider/code.js +58 -0
  12. package/dist/esm/provider/cognito.js +16 -0
  13. package/dist/esm/provider/discord.js +15 -0
  14. package/dist/esm/provider/facebook.js +24 -0
  15. package/dist/esm/provider/github.js +15 -0
  16. package/dist/esm/provider/google.js +25 -0
  17. package/dist/esm/provider/index.js +3 -0
  18. package/dist/esm/provider/jumpcloud.js +15 -0
  19. package/dist/esm/provider/keycloak.js +15 -0
  20. package/dist/esm/provider/linkedin.js +15 -0
  21. package/dist/esm/provider/m2m.js +17 -0
  22. package/dist/esm/provider/microsoft.js +24 -0
  23. package/dist/esm/provider/oauth2.js +119 -0
  24. package/dist/esm/provider/oidc.js +69 -0
  25. package/dist/esm/provider/passkey.js +315 -0
  26. package/dist/esm/provider/password.js +306 -0
  27. package/dist/esm/provider/provider.js +10 -0
  28. package/dist/esm/provider/slack.js +15 -0
  29. package/dist/esm/provider/spotify.js +15 -0
  30. package/dist/esm/provider/twitch.js +15 -0
  31. package/dist/esm/provider/x.js +16 -0
  32. package/dist/esm/provider/yahoo.js +15 -0
  33. package/dist/esm/random.js +27 -0
  34. package/dist/esm/storage/aws.js +39 -0
  35. package/dist/esm/storage/cloudflare.js +42 -0
  36. package/dist/esm/storage/dynamo.js +116 -0
  37. package/dist/esm/storage/memory.js +88 -0
  38. package/dist/esm/storage/storage.js +36 -0
  39. package/dist/esm/subject.js +7 -0
  40. package/dist/esm/ui/base.js +407 -0
  41. package/dist/esm/ui/code.js +151 -0
  42. package/dist/esm/ui/form.js +43 -0
  43. package/dist/esm/ui/icon.js +92 -0
  44. package/dist/esm/ui/passkey.js +329 -0
  45. package/dist/esm/ui/password.js +338 -0
  46. package/dist/esm/ui/select.js +187 -0
  47. package/dist/esm/ui/theme.js +115 -0
  48. package/dist/esm/util.js +54 -0
  49. package/dist/types/client.d.ts +466 -0
  50. package/dist/types/client.d.ts.map +1 -0
  51. package/dist/types/error.d.ts +77 -0
  52. package/dist/types/error.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +20 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/issuer.d.ts +465 -0
  56. package/dist/types/issuer.d.ts.map +1 -0
  57. package/dist/types/jwt.d.ts +6 -0
  58. package/dist/types/jwt.d.ts.map +1 -0
  59. package/dist/types/keys.d.ts +18 -0
  60. package/dist/types/keys.d.ts.map +1 -0
  61. package/dist/types/pkce.d.ts +7 -0
  62. package/dist/types/pkce.d.ts.map +1 -0
  63. package/dist/types/provider/apple.d.ts +108 -0
  64. package/dist/types/provider/apple.d.ts.map +1 -0
  65. package/dist/types/provider/arctic.d.ts +16 -0
  66. package/dist/types/provider/arctic.d.ts.map +1 -0
  67. package/dist/types/provider/code.d.ts +74 -0
  68. package/dist/types/provider/code.d.ts.map +1 -0
  69. package/dist/types/provider/cognito.d.ts +64 -0
  70. package/dist/types/provider/cognito.d.ts.map +1 -0
  71. package/dist/types/provider/discord.d.ts +38 -0
  72. package/dist/types/provider/discord.d.ts.map +1 -0
  73. package/dist/types/provider/facebook.d.ts +74 -0
  74. package/dist/types/provider/facebook.d.ts.map +1 -0
  75. package/dist/types/provider/github.d.ts +38 -0
  76. package/dist/types/provider/github.d.ts.map +1 -0
  77. package/dist/types/provider/google.d.ts +74 -0
  78. package/dist/types/provider/google.d.ts.map +1 -0
  79. package/dist/types/provider/index.d.ts +4 -0
  80. package/dist/types/provider/index.d.ts.map +1 -0
  81. package/dist/types/provider/jumpcloud.d.ts +38 -0
  82. package/dist/types/provider/jumpcloud.d.ts.map +1 -0
  83. package/dist/types/provider/keycloak.d.ts +67 -0
  84. package/dist/types/provider/keycloak.d.ts.map +1 -0
  85. package/dist/types/provider/linkedin.d.ts +6 -0
  86. package/dist/types/provider/linkedin.d.ts.map +1 -0
  87. package/dist/types/provider/m2m.d.ts +34 -0
  88. package/dist/types/provider/m2m.d.ts.map +1 -0
  89. package/dist/types/provider/microsoft.d.ts +89 -0
  90. package/dist/types/provider/microsoft.d.ts.map +1 -0
  91. package/dist/types/provider/oauth2.d.ts +133 -0
  92. package/dist/types/provider/oauth2.d.ts.map +1 -0
  93. package/dist/types/provider/oidc.d.ts +91 -0
  94. package/dist/types/provider/oidc.d.ts.map +1 -0
  95. package/dist/types/provider/passkey.d.ts +143 -0
  96. package/dist/types/provider/passkey.d.ts.map +1 -0
  97. package/dist/types/provider/password.d.ts +210 -0
  98. package/dist/types/provider/password.d.ts.map +1 -0
  99. package/dist/types/provider/provider.d.ts +29 -0
  100. package/dist/types/provider/provider.d.ts.map +1 -0
  101. package/dist/types/provider/slack.d.ts +59 -0
  102. package/dist/types/provider/slack.d.ts.map +1 -0
  103. package/dist/types/provider/spotify.d.ts +38 -0
  104. package/dist/types/provider/spotify.d.ts.map +1 -0
  105. package/dist/types/provider/twitch.d.ts +38 -0
  106. package/dist/types/provider/twitch.d.ts.map +1 -0
  107. package/dist/types/provider/x.d.ts +38 -0
  108. package/dist/types/provider/x.d.ts.map +1 -0
  109. package/dist/types/provider/yahoo.d.ts +38 -0
  110. package/dist/types/provider/yahoo.d.ts.map +1 -0
  111. package/dist/types/random.d.ts +3 -0
  112. package/dist/types/random.d.ts.map +1 -0
  113. package/dist/types/storage/aws.d.ts +4 -0
  114. package/dist/types/storage/aws.d.ts.map +1 -0
  115. package/dist/types/storage/cloudflare.d.ts +34 -0
  116. package/dist/types/storage/cloudflare.d.ts.map +1 -0
  117. package/dist/types/storage/dynamo.d.ts +65 -0
  118. package/dist/types/storage/dynamo.d.ts.map +1 -0
  119. package/dist/types/storage/memory.d.ts +49 -0
  120. package/dist/types/storage/memory.d.ts.map +1 -0
  121. package/dist/types/storage/storage.d.ts +15 -0
  122. package/dist/types/storage/storage.d.ts.map +1 -0
  123. package/dist/types/subject.d.ts +122 -0
  124. package/dist/types/subject.d.ts.map +1 -0
  125. package/dist/types/ui/base.d.ts +5 -0
  126. package/dist/types/ui/base.d.ts.map +1 -0
  127. package/dist/types/ui/code.d.ts +104 -0
  128. package/dist/types/ui/code.d.ts.map +1 -0
  129. package/dist/types/ui/form.d.ts +6 -0
  130. package/dist/types/ui/form.d.ts.map +1 -0
  131. package/dist/types/ui/icon.d.ts +6 -0
  132. package/dist/types/ui/icon.d.ts.map +1 -0
  133. package/dist/types/ui/passkey.d.ts +5 -0
  134. package/dist/types/ui/passkey.d.ts.map +1 -0
  135. package/dist/types/ui/password.d.ts +139 -0
  136. package/dist/types/ui/password.d.ts.map +1 -0
  137. package/dist/types/ui/select.d.ts +55 -0
  138. package/dist/types/ui/select.d.ts.map +1 -0
  139. package/dist/types/ui/theme.d.ts +207 -0
  140. package/dist/types/ui/theme.d.ts.map +1 -0
  141. package/dist/types/util.d.ts +8 -0
  142. package/dist/types/util.d.ts.map +1 -0
  143. package/package.json +51 -0
  144. package/src/client.ts +749 -0
  145. package/src/css.d.ts +4 -0
  146. package/src/error.ts +120 -0
  147. package/src/index.ts +26 -0
  148. package/src/issuer.ts +1302 -0
  149. package/src/jwt.ts +17 -0
  150. package/src/keys.ts +139 -0
  151. package/src/pkce.ts +40 -0
  152. package/src/provider/apple.ts +127 -0
  153. package/src/provider/arctic.ts +66 -0
  154. package/src/provider/code.ts +227 -0
  155. package/src/provider/cognito.ts +74 -0
  156. package/src/provider/discord.ts +45 -0
  157. package/src/provider/facebook.ts +84 -0
  158. package/src/provider/github.ts +45 -0
  159. package/src/provider/google.ts +85 -0
  160. package/src/provider/index.ts +3 -0
  161. package/src/provider/jumpcloud.ts +45 -0
  162. package/src/provider/keycloak.ts +75 -0
  163. package/src/provider/linkedin.ts +12 -0
  164. package/src/provider/m2m.ts +56 -0
  165. package/src/provider/microsoft.ts +100 -0
  166. package/src/provider/oauth2.ts +297 -0
  167. package/src/provider/oidc.ts +179 -0
  168. package/src/provider/passkey.ts +655 -0
  169. package/src/provider/password.ts +672 -0
  170. package/src/provider/provider.ts +33 -0
  171. package/src/provider/slack.ts +67 -0
  172. package/src/provider/spotify.ts +45 -0
  173. package/src/provider/twitch.ts +45 -0
  174. package/src/provider/x.ts +46 -0
  175. package/src/provider/yahoo.ts +45 -0
  176. package/src/random.ts +24 -0
  177. package/src/storage/aws.ts +59 -0
  178. package/src/storage/cloudflare.ts +77 -0
  179. package/src/storage/dynamo.ts +193 -0
  180. package/src/storage/memory.ts +135 -0
  181. package/src/storage/storage.ts +46 -0
  182. package/src/subject.ts +130 -0
  183. package/src/ui/base.tsx +118 -0
  184. package/src/ui/code.tsx +215 -0
  185. package/src/ui/form.tsx +40 -0
  186. package/src/ui/icon.tsx +95 -0
  187. package/src/ui/passkey.tsx +321 -0
  188. package/src/ui/password.tsx +405 -0
  189. package/src/ui/select.tsx +221 -0
  190. package/src/ui/theme.ts +319 -0
  191. package/src/ui/ui.css +252 -0
  192. 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
+ }