@nixxie-cms/auth-oauth 1.0.1 → 1.1.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/README.md +37 -0
- package/dist/declarations/src/express.d.ts +82 -0
- package/dist/declarations/src/express.d.ts.map +1 -0
- package/dist/declarations/src/index.d.ts +2 -0
- package/dist/declarations/src/index.d.ts.map +1 -1
- package/dist/nixxie-cms-auth-oauth.cjs.js +180 -0
- package/dist/nixxie-cms-auth-oauth.esm.js +180 -2
- package/package.json +2 -2
- package/src/express.ts +220 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -33,3 +33,40 @@ app.get('/auth/google/callback', async (req, res) => {
|
|
|
33
33
|
// upsert a user keyed by profile.id / profile.email and start a session
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
|
+
|
|
37
|
+
## Automatic routes
|
|
38
|
+
|
|
39
|
+
Skip the hand-written routes entirely: `installOAuthRoutes` mounts
|
|
40
|
+
`GET /auth/:provider` (redirect to consent screen) and `GET /auth/:provider/callback`
|
|
41
|
+
(code exchange, find-or-create user, session start) for every configured provider, with
|
|
42
|
+
signed-cookie state validation built in.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { installOAuthRoutes } from '@nixxie-cms/auth-oauth'
|
|
46
|
+
|
|
47
|
+
server: {
|
|
48
|
+
extendExpressApp: (app, context) => {
|
|
49
|
+
installOAuthRoutes(app, context, { oauth, listKey: 'User' })
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or as a plugin:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { oauthPlugin } from '@nixxie-cms/auth-oauth'
|
|
58
|
+
|
|
59
|
+
config({
|
|
60
|
+
plugins: [oauthPlugin({ oauth, listKey: 'User' })],
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Users are matched by the profile's email against `identityField` (default `'email'`) and
|
|
65
|
+
created on first sign-in unless `allowSignup: false`. Sessions are started via the
|
|
66
|
+
configured session strategy with `{ itemId }`, compatible with `createAuth` sessions.
|
|
67
|
+
|
|
68
|
+
Options: `listKey` (default `'User'`), `identityField`, `allowSignup`, `resolveUser`
|
|
69
|
+
(data for users created on first sign-in — return `null` to reject), `onSignIn`
|
|
70
|
+
(post-sign-in hook with `{ provider, itemId, created, profile, tokens, context }`),
|
|
71
|
+
`basePath` (default `'/auth'`), `successRedirect` (default `'/'`) and `failureRedirect`
|
|
72
|
+
(default `'/signin'`, gets `?error=<code>` appended).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { NixxieContext, NixxiePlugin } from '@nixxie-cms/core/types';
|
|
2
|
+
import type { OAuth } from "./index.js";
|
|
3
|
+
import type { OAuthProfile, OAuthTokens } from "./types.js";
|
|
4
|
+
export type OAuthRoutesOptions = {
|
|
5
|
+
/** The OAuth client created with `createOAuth()`. */
|
|
6
|
+
oauth: OAuth;
|
|
7
|
+
/**
|
|
8
|
+
* Collection holding your users.
|
|
9
|
+
* @default 'User'
|
|
10
|
+
*/
|
|
11
|
+
listKey?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Field used to match OAuth profiles to existing users (by the profile's email).
|
|
14
|
+
* @default 'email'
|
|
15
|
+
*/
|
|
16
|
+
identityField?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Create users on first sign-in. When false, sign-ins from unknown
|
|
19
|
+
* profiles are redirected to `failureRedirect`.
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
allowSignup?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the data for a user being created on first sign-in.
|
|
25
|
+
* Return null to reject the sign-in. Defaults to `{ [identityField]: profile.email }`
|
|
26
|
+
* plus `name` when the collection's input accepts it.
|
|
27
|
+
*/
|
|
28
|
+
resolveUser?: (args: {
|
|
29
|
+
provider: string;
|
|
30
|
+
profile: OAuthProfile;
|
|
31
|
+
tokens: OAuthTokens;
|
|
32
|
+
context: NixxieContext;
|
|
33
|
+
}) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;
|
|
34
|
+
/**
|
|
35
|
+
* Called after a successful sign-in (e.g. to link accounts or audit). Receives the
|
|
36
|
+
* signed-in user's id.
|
|
37
|
+
*/
|
|
38
|
+
onSignIn?: (args: {
|
|
39
|
+
provider: string;
|
|
40
|
+
itemId: string;
|
|
41
|
+
created: boolean;
|
|
42
|
+
profile: OAuthProfile;
|
|
43
|
+
tokens: OAuthTokens;
|
|
44
|
+
context: NixxieContext;
|
|
45
|
+
}) => Promise<void> | void;
|
|
46
|
+
/**
|
|
47
|
+
* Base path the routes are mounted under: `<basePath>/:provider` starts the flow and
|
|
48
|
+
* `<basePath>/:provider/callback` completes it.
|
|
49
|
+
* @default '/auth'
|
|
50
|
+
*/
|
|
51
|
+
basePath?: string;
|
|
52
|
+
/** Where to send the user after a successful sign-in. @default '/' */
|
|
53
|
+
successRedirect?: string;
|
|
54
|
+
/** Where to send the user after a failure (gets `?error=<code>` appended). @default '/signin' */
|
|
55
|
+
failureRedirect?: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Mount ready-made OAuth sign-in routes on the Express app:
|
|
59
|
+
*
|
|
60
|
+
* - `GET <basePath>/:provider` — redirects to the provider's consent screen
|
|
61
|
+
* - `GET <basePath>/:provider/callback` — exchanges the code, finds-or-creates the
|
|
62
|
+
* user, starts a session (compatible with `createAuth` sessions: `{ itemId }`),
|
|
63
|
+
* and redirects
|
|
64
|
+
*
|
|
65
|
+
* Call from `server.extendExpressApp`, or use `oauthPlugin()` to wire it automatically.
|
|
66
|
+
*/
|
|
67
|
+
export declare function installOAuthRoutes(app: any, context: NixxieContext, options: OAuthRoutesOptions): void;
|
|
68
|
+
/**
|
|
69
|
+
* The same OAuth routes as `installOAuthRoutes`, packaged as a Nixxie plugin.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* config({
|
|
73
|
+
* plugins: [
|
|
74
|
+
* oauthPlugin({
|
|
75
|
+
* oauth: createOAuth({ providers: { google: { provider: 'google', ... } } }),
|
|
76
|
+
* listKey: 'User',
|
|
77
|
+
* }),
|
|
78
|
+
* ],
|
|
79
|
+
* })
|
|
80
|
+
*/
|
|
81
|
+
export declare function oauthPlugin(options: OAuthRoutesOptions): NixxiePlugin;
|
|
82
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"../../../src","sources":["express.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,mBAAe;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAe;AAIxD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,qDAAqD;IACrD,KAAK,EAAE,KAAK,CAAA;IACZ;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QACnB,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,YAAY,CAAA;QACrB,MAAM,EAAE,WAAW,CAAA;QACnB,OAAO,EAAE,aAAa,CAAA;KACvB,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC9E;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,YAAY,CAAA;QACrB,MAAM,EAAE,WAAW,CAAA;QACnB,OAAO,EAAE,aAAa,CAAA;KACvB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAaD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,kBAAkB,GAC1B,IAAI,CAwGN;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,YAAY,CAcrE"}
|
|
@@ -24,6 +24,8 @@ export declare class OAuth {
|
|
|
24
24
|
}>;
|
|
25
25
|
}
|
|
26
26
|
export declare function createOAuth(config: OAuthConfig): OAuth;
|
|
27
|
+
export { installOAuthRoutes, oauthPlugin } from "./express.js";
|
|
28
|
+
export type { OAuthRoutesOptions } from "./express.js";
|
|
27
29
|
export { builtInProviders as providers };
|
|
28
30
|
export type { OAuthConfig, OAuthProviderConfig, OAuthProviderDefinition, OAuthProviderName, OAuthProfile, OAuthTokens, } from "./types.js";
|
|
29
31
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"../../../src","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,uBAAmB;AAC3D,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EAGZ,WAAW,EACZ,mBAAe;AAQhB,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAa;gBAEf,MAAM,EAAE,WAAW;IAI/B,yCAAyC;IACzC,IAAI,IAAI,MAAM,EAAE;IAIhB,OAAO,CAAC,GAAG;IAMX,0FAA0F;IAC1F,WAAW,IAAI,MAAM;IAIrB,wEAAwE;IACxE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,MAAM;IAa1G,8EAA8E;IACxE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IA+BpE,+DAA+D;IACzD,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAc5E,wEAAwE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC;CAKpG;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAEtD;AAED,OAAO,EAAE,gBAAgB,IAAI,SAAS,EAAE,CAAA;AACxC,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,mBAAe"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"../../../src","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,uBAAmB;AAC3D,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EAGZ,WAAW,EACZ,mBAAe;AAQhB,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAa;gBAEf,MAAM,EAAE,WAAW;IAI/B,yCAAyC;IACzC,IAAI,IAAI,MAAM,EAAE;IAIhB,OAAO,CAAC,GAAG;IAMX,0FAA0F;IAC1F,WAAW,IAAI,MAAM;IAIrB,wEAAwE;IACxE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,MAAM;IAa1G,8EAA8E;IACxE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IA+BpE,+DAA+D;IACzD,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAc5E,wEAAwE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC;CAKpG;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAEtD;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,qBAAiB;AAC3D,YAAY,EAAE,kBAAkB,EAAE,qBAAiB;AACnD,OAAO,EAAE,gBAAgB,IAAI,SAAS,EAAE,CAAA;AACxC,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,mBAAe"}
|
|
@@ -97,6 +97,184 @@ const providers = {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
const STATE_COOKIE = 'nixxie-oauth-state';
|
|
101
|
+
function parseCookies(header) {
|
|
102
|
+
const out = {};
|
|
103
|
+
if (!header) return out;
|
|
104
|
+
for (const part of header.split(';')) {
|
|
105
|
+
const eq = part.indexOf('=');
|
|
106
|
+
if (eq === -1) continue;
|
|
107
|
+
out[part.slice(0, eq).trim()] = decodeURIComponent(part.slice(eq + 1).trim());
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mount ready-made OAuth sign-in routes on the Express app:
|
|
114
|
+
*
|
|
115
|
+
* - `GET <basePath>/:provider` — redirects to the provider's consent screen
|
|
116
|
+
* - `GET <basePath>/:provider/callback` — exchanges the code, finds-or-creates the
|
|
117
|
+
* user, starts a session (compatible with `createAuth` sessions: `{ itemId }`),
|
|
118
|
+
* and redirects
|
|
119
|
+
*
|
|
120
|
+
* Call from `server.extendExpressApp`, or use `oauthPlugin()` to wire it automatically.
|
|
121
|
+
*/
|
|
122
|
+
function installOAuthRoutes(app, context, options) {
|
|
123
|
+
const {
|
|
124
|
+
oauth,
|
|
125
|
+
listKey = 'User',
|
|
126
|
+
identityField = 'email',
|
|
127
|
+
allowSignup = true,
|
|
128
|
+
resolveUser,
|
|
129
|
+
onSignIn,
|
|
130
|
+
basePath = '/auth',
|
|
131
|
+
successRedirect = '/',
|
|
132
|
+
failureRedirect = '/signin'
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
// The state value is signed so the callback can verify it even though the cookie
|
|
136
|
+
// itself is the only storage — no server-side flow state needed.
|
|
137
|
+
const stateSecret = node_crypto.randomBytes(32).toString('hex');
|
|
138
|
+
const signState = value => `${value}.${node_crypto.createHmac('sha256', stateSecret).update(value).digest('hex')}`;
|
|
139
|
+
const verifyState = signed => {
|
|
140
|
+
if (!signed) return;
|
|
141
|
+
const at = signed.lastIndexOf('.');
|
|
142
|
+
if (at === -1) return;
|
|
143
|
+
const value = signed.slice(0, at);
|
|
144
|
+
const mac = node_crypto.createHmac('sha256', stateSecret).update(value).digest('hex');
|
|
145
|
+
return mac === signed.slice(at + 1) ? value : undefined;
|
|
146
|
+
};
|
|
147
|
+
const fail = (res, code) => {
|
|
148
|
+
const sep = failureRedirect.includes('?') ? '&' : '?';
|
|
149
|
+
res.redirect(`${failureRedirect}${sep}error=${encodeURIComponent(code)}`);
|
|
150
|
+
};
|
|
151
|
+
app.get(`${basePath}/:provider`, (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
var _res$cookie;
|
|
154
|
+
const provider = req.params.provider;
|
|
155
|
+
if (!oauth.list().includes(provider)) return fail(res, 'unknown-provider');
|
|
156
|
+
const state = oauth.createState();
|
|
157
|
+
(_res$cookie = res.cookie) === null || _res$cookie === void 0 || _res$cookie.call(res, STATE_COOKIE, signState(`${provider}:${state}`), {
|
|
158
|
+
httpOnly: true,
|
|
159
|
+
sameSite: 'lax',
|
|
160
|
+
secure: process.env.NODE_ENV === 'production',
|
|
161
|
+
maxAge: 10 * 60 * 1000,
|
|
162
|
+
path: basePath
|
|
163
|
+
});
|
|
164
|
+
res.redirect(oauth.authorizationUrl(provider, {
|
|
165
|
+
state
|
|
166
|
+
}));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('[nixxie/auth-oauth] start failed:', err);
|
|
169
|
+
fail(res, 'oauth-start-failed');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
app.get(`${basePath}/:provider/callback`, (req, res) => {
|
|
173
|
+
void (async (_req$query, _res$clearCookie) => {
|
|
174
|
+
const provider = req.params.provider;
|
|
175
|
+
const {
|
|
176
|
+
code,
|
|
177
|
+
state,
|
|
178
|
+
error
|
|
179
|
+
} = (_req$query = req.query) !== null && _req$query !== void 0 ? _req$query : {};
|
|
180
|
+
if (error) return fail(res, String(error));
|
|
181
|
+
if (!code) return fail(res, 'missing-code');
|
|
182
|
+
const stored = verifyState(parseCookies(req.headers.cookie)[STATE_COOKIE]);
|
|
183
|
+
(_res$clearCookie = res.clearCookie) === null || _res$clearCookie === void 0 || _res$clearCookie.call(res, STATE_COOKIE, {
|
|
184
|
+
path: basePath
|
|
185
|
+
});
|
|
186
|
+
if (!stored || stored !== `${provider}:${state}`) return fail(res, 'invalid-state');
|
|
187
|
+
const {
|
|
188
|
+
tokens,
|
|
189
|
+
profile
|
|
190
|
+
} = await oauth.callback(provider, String(code));
|
|
191
|
+
if (!profile.email) return fail(res, 'no-email');
|
|
192
|
+
const requestContext = await context.withRequest(req, res);
|
|
193
|
+
const sudo = requestContext.sudo();
|
|
194
|
+
const existing = await sudo.db[listKey].findMany({
|
|
195
|
+
where: {
|
|
196
|
+
[identityField]: {
|
|
197
|
+
equals: profile.email
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
take: 1
|
|
201
|
+
});
|
|
202
|
+
let itemId;
|
|
203
|
+
let created = false;
|
|
204
|
+
if (existing.length) {
|
|
205
|
+
itemId = String(existing[0].id);
|
|
206
|
+
} else {
|
|
207
|
+
if (!allowSignup) return fail(res, 'unknown-user');
|
|
208
|
+
const data = resolveUser ? await resolveUser({
|
|
209
|
+
provider,
|
|
210
|
+
profile,
|
|
211
|
+
tokens,
|
|
212
|
+
context: requestContext
|
|
213
|
+
}) : {
|
|
214
|
+
[identityField]: profile.email
|
|
215
|
+
};
|
|
216
|
+
if (!data) return fail(res, 'signup-rejected');
|
|
217
|
+
const createdItem = await sudo.db[listKey].createOne({
|
|
218
|
+
data: data
|
|
219
|
+
});
|
|
220
|
+
itemId = String(createdItem.id);
|
|
221
|
+
created = true;
|
|
222
|
+
}
|
|
223
|
+
if (!requestContext.sessionStrategy) {
|
|
224
|
+
throw new Error('installOAuthRoutes: no session strategy configured');
|
|
225
|
+
}
|
|
226
|
+
await requestContext.sessionStrategy.start({
|
|
227
|
+
context: requestContext,
|
|
228
|
+
data: {
|
|
229
|
+
itemId
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
await (onSignIn === null || onSignIn === void 0 ? void 0 : onSignIn({
|
|
233
|
+
provider,
|
|
234
|
+
itemId,
|
|
235
|
+
created,
|
|
236
|
+
profile,
|
|
237
|
+
tokens,
|
|
238
|
+
context: requestContext
|
|
239
|
+
}));
|
|
240
|
+
res.redirect(successRedirect);
|
|
241
|
+
})().catch(err => {
|
|
242
|
+
console.error('[nixxie/auth-oauth] callback failed:', err);
|
|
243
|
+
fail(res, 'oauth-failed');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The same OAuth routes as `installOAuthRoutes`, packaged as a Nixxie plugin.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* config({
|
|
253
|
+
* plugins: [
|
|
254
|
+
* oauthPlugin({
|
|
255
|
+
* oauth: createOAuth({ providers: { google: { provider: 'google', ... } } }),
|
|
256
|
+
* listKey: 'User',
|
|
257
|
+
* }),
|
|
258
|
+
* ],
|
|
259
|
+
* })
|
|
260
|
+
*/
|
|
261
|
+
function oauthPlugin(options) {
|
|
262
|
+
return {
|
|
263
|
+
name: 'nixxie-auth-oauth',
|
|
264
|
+
extendConfig: config => ({
|
|
265
|
+
...config,
|
|
266
|
+
server: {
|
|
267
|
+
...config.server,
|
|
268
|
+
extendExpressApp: async (app, context) => {
|
|
269
|
+
var _config$server, _config$server$extend;
|
|
270
|
+
await ((_config$server = config.server) === null || _config$server === void 0 || (_config$server$extend = _config$server.extendExpressApp) === null || _config$server$extend === void 0 ? void 0 : _config$server$extend.call(_config$server, app, context));
|
|
271
|
+
installOAuthRoutes(app, context, options);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
100
278
|
function resolveDefinition(cfg) {
|
|
101
279
|
if (cfg.definition) return cfg.definition;
|
|
102
280
|
if (cfg.provider) return providers[cfg.provider];
|
|
@@ -214,4 +392,6 @@ function createOAuth(config) {
|
|
|
214
392
|
|
|
215
393
|
exports.OAuth = OAuth;
|
|
216
394
|
exports.createOAuth = createOAuth;
|
|
395
|
+
exports.installOAuthRoutes = installOAuthRoutes;
|
|
396
|
+
exports.oauthPlugin = oauthPlugin;
|
|
217
397
|
exports.providers = providers;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { randomBytes, createHmac } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
const providers = {
|
|
4
4
|
google: {
|
|
@@ -93,6 +93,184 @@ const providers = {
|
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
const STATE_COOKIE = 'nixxie-oauth-state';
|
|
97
|
+
function parseCookies(header) {
|
|
98
|
+
const out = {};
|
|
99
|
+
if (!header) return out;
|
|
100
|
+
for (const part of header.split(';')) {
|
|
101
|
+
const eq = part.indexOf('=');
|
|
102
|
+
if (eq === -1) continue;
|
|
103
|
+
out[part.slice(0, eq).trim()] = decodeURIComponent(part.slice(eq + 1).trim());
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mount ready-made OAuth sign-in routes on the Express app:
|
|
110
|
+
*
|
|
111
|
+
* - `GET <basePath>/:provider` — redirects to the provider's consent screen
|
|
112
|
+
* - `GET <basePath>/:provider/callback` — exchanges the code, finds-or-creates the
|
|
113
|
+
* user, starts a session (compatible with `createAuth` sessions: `{ itemId }`),
|
|
114
|
+
* and redirects
|
|
115
|
+
*
|
|
116
|
+
* Call from `server.extendExpressApp`, or use `oauthPlugin()` to wire it automatically.
|
|
117
|
+
*/
|
|
118
|
+
function installOAuthRoutes(app, context, options) {
|
|
119
|
+
const {
|
|
120
|
+
oauth,
|
|
121
|
+
listKey = 'User',
|
|
122
|
+
identityField = 'email',
|
|
123
|
+
allowSignup = true,
|
|
124
|
+
resolveUser,
|
|
125
|
+
onSignIn,
|
|
126
|
+
basePath = '/auth',
|
|
127
|
+
successRedirect = '/',
|
|
128
|
+
failureRedirect = '/signin'
|
|
129
|
+
} = options;
|
|
130
|
+
|
|
131
|
+
// The state value is signed so the callback can verify it even though the cookie
|
|
132
|
+
// itself is the only storage — no server-side flow state needed.
|
|
133
|
+
const stateSecret = randomBytes(32).toString('hex');
|
|
134
|
+
const signState = value => `${value}.${createHmac('sha256', stateSecret).update(value).digest('hex')}`;
|
|
135
|
+
const verifyState = signed => {
|
|
136
|
+
if (!signed) return;
|
|
137
|
+
const at = signed.lastIndexOf('.');
|
|
138
|
+
if (at === -1) return;
|
|
139
|
+
const value = signed.slice(0, at);
|
|
140
|
+
const mac = createHmac('sha256', stateSecret).update(value).digest('hex');
|
|
141
|
+
return mac === signed.slice(at + 1) ? value : undefined;
|
|
142
|
+
};
|
|
143
|
+
const fail = (res, code) => {
|
|
144
|
+
const sep = failureRedirect.includes('?') ? '&' : '?';
|
|
145
|
+
res.redirect(`${failureRedirect}${sep}error=${encodeURIComponent(code)}`);
|
|
146
|
+
};
|
|
147
|
+
app.get(`${basePath}/:provider`, (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
var _res$cookie;
|
|
150
|
+
const provider = req.params.provider;
|
|
151
|
+
if (!oauth.list().includes(provider)) return fail(res, 'unknown-provider');
|
|
152
|
+
const state = oauth.createState();
|
|
153
|
+
(_res$cookie = res.cookie) === null || _res$cookie === void 0 || _res$cookie.call(res, STATE_COOKIE, signState(`${provider}:${state}`), {
|
|
154
|
+
httpOnly: true,
|
|
155
|
+
sameSite: 'lax',
|
|
156
|
+
secure: process.env.NODE_ENV === 'production',
|
|
157
|
+
maxAge: 10 * 60 * 1000,
|
|
158
|
+
path: basePath
|
|
159
|
+
});
|
|
160
|
+
res.redirect(oauth.authorizationUrl(provider, {
|
|
161
|
+
state
|
|
162
|
+
}));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('[nixxie/auth-oauth] start failed:', err);
|
|
165
|
+
fail(res, 'oauth-start-failed');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
app.get(`${basePath}/:provider/callback`, (req, res) => {
|
|
169
|
+
void (async (_req$query, _res$clearCookie) => {
|
|
170
|
+
const provider = req.params.provider;
|
|
171
|
+
const {
|
|
172
|
+
code,
|
|
173
|
+
state,
|
|
174
|
+
error
|
|
175
|
+
} = (_req$query = req.query) !== null && _req$query !== void 0 ? _req$query : {};
|
|
176
|
+
if (error) return fail(res, String(error));
|
|
177
|
+
if (!code) return fail(res, 'missing-code');
|
|
178
|
+
const stored = verifyState(parseCookies(req.headers.cookie)[STATE_COOKIE]);
|
|
179
|
+
(_res$clearCookie = res.clearCookie) === null || _res$clearCookie === void 0 || _res$clearCookie.call(res, STATE_COOKIE, {
|
|
180
|
+
path: basePath
|
|
181
|
+
});
|
|
182
|
+
if (!stored || stored !== `${provider}:${state}`) return fail(res, 'invalid-state');
|
|
183
|
+
const {
|
|
184
|
+
tokens,
|
|
185
|
+
profile
|
|
186
|
+
} = await oauth.callback(provider, String(code));
|
|
187
|
+
if (!profile.email) return fail(res, 'no-email');
|
|
188
|
+
const requestContext = await context.withRequest(req, res);
|
|
189
|
+
const sudo = requestContext.sudo();
|
|
190
|
+
const existing = await sudo.db[listKey].findMany({
|
|
191
|
+
where: {
|
|
192
|
+
[identityField]: {
|
|
193
|
+
equals: profile.email
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
take: 1
|
|
197
|
+
});
|
|
198
|
+
let itemId;
|
|
199
|
+
let created = false;
|
|
200
|
+
if (existing.length) {
|
|
201
|
+
itemId = String(existing[0].id);
|
|
202
|
+
} else {
|
|
203
|
+
if (!allowSignup) return fail(res, 'unknown-user');
|
|
204
|
+
const data = resolveUser ? await resolveUser({
|
|
205
|
+
provider,
|
|
206
|
+
profile,
|
|
207
|
+
tokens,
|
|
208
|
+
context: requestContext
|
|
209
|
+
}) : {
|
|
210
|
+
[identityField]: profile.email
|
|
211
|
+
};
|
|
212
|
+
if (!data) return fail(res, 'signup-rejected');
|
|
213
|
+
const createdItem = await sudo.db[listKey].createOne({
|
|
214
|
+
data: data
|
|
215
|
+
});
|
|
216
|
+
itemId = String(createdItem.id);
|
|
217
|
+
created = true;
|
|
218
|
+
}
|
|
219
|
+
if (!requestContext.sessionStrategy) {
|
|
220
|
+
throw new Error('installOAuthRoutes: no session strategy configured');
|
|
221
|
+
}
|
|
222
|
+
await requestContext.sessionStrategy.start({
|
|
223
|
+
context: requestContext,
|
|
224
|
+
data: {
|
|
225
|
+
itemId
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
await (onSignIn === null || onSignIn === void 0 ? void 0 : onSignIn({
|
|
229
|
+
provider,
|
|
230
|
+
itemId,
|
|
231
|
+
created,
|
|
232
|
+
profile,
|
|
233
|
+
tokens,
|
|
234
|
+
context: requestContext
|
|
235
|
+
}));
|
|
236
|
+
res.redirect(successRedirect);
|
|
237
|
+
})().catch(err => {
|
|
238
|
+
console.error('[nixxie/auth-oauth] callback failed:', err);
|
|
239
|
+
fail(res, 'oauth-failed');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The same OAuth routes as `installOAuthRoutes`, packaged as a Nixxie plugin.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* config({
|
|
249
|
+
* plugins: [
|
|
250
|
+
* oauthPlugin({
|
|
251
|
+
* oauth: createOAuth({ providers: { google: { provider: 'google', ... } } }),
|
|
252
|
+
* listKey: 'User',
|
|
253
|
+
* }),
|
|
254
|
+
* ],
|
|
255
|
+
* })
|
|
256
|
+
*/
|
|
257
|
+
function oauthPlugin(options) {
|
|
258
|
+
return {
|
|
259
|
+
name: 'nixxie-auth-oauth',
|
|
260
|
+
extendConfig: config => ({
|
|
261
|
+
...config,
|
|
262
|
+
server: {
|
|
263
|
+
...config.server,
|
|
264
|
+
extendExpressApp: async (app, context) => {
|
|
265
|
+
var _config$server, _config$server$extend;
|
|
266
|
+
await ((_config$server = config.server) === null || _config$server === void 0 || (_config$server$extend = _config$server.extendExpressApp) === null || _config$server$extend === void 0 ? void 0 : _config$server$extend.call(_config$server, app, context));
|
|
267
|
+
installOAuthRoutes(app, context, options);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
96
274
|
function resolveDefinition(cfg) {
|
|
97
275
|
if (cfg.definition) return cfg.definition;
|
|
98
276
|
if (cfg.provider) return providers[cfg.provider];
|
|
@@ -208,4 +386,4 @@ function createOAuth(config) {
|
|
|
208
386
|
return new OAuth(config);
|
|
209
387
|
}
|
|
210
388
|
|
|
211
|
-
export { OAuth, createOAuth, providers };
|
|
389
|
+
export { OAuth, createOAuth, installOAuthRoutes, oauthPlugin, providers };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nixxie-cms/auth-oauth",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/nixxie-cms-auth-oauth.cjs.js",
|
|
6
6
|
"module": "dist/nixxie-cms-auth-oauth.esm.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@babel/runtime": "^7.24.7"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@nixxie-cms/core": "^1.0
|
|
19
|
+
"@nixxie-cms/core": "^1.1.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@nixxie-cms/core": "^1.0.1"
|
package/src/express.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from 'node:crypto'
|
|
2
|
+
import type { NixxieContext, NixxiePlugin } from '@nixxie-cms/core/types'
|
|
3
|
+
import type { OAuth } from './index'
|
|
4
|
+
import type { OAuthProfile, OAuthTokens } from './types'
|
|
5
|
+
|
|
6
|
+
const STATE_COOKIE = 'nixxie-oauth-state'
|
|
7
|
+
|
|
8
|
+
export type OAuthRoutesOptions = {
|
|
9
|
+
/** The OAuth client created with `createOAuth()`. */
|
|
10
|
+
oauth: OAuth
|
|
11
|
+
/**
|
|
12
|
+
* Collection holding your users.
|
|
13
|
+
* @default 'User'
|
|
14
|
+
*/
|
|
15
|
+
listKey?: string
|
|
16
|
+
/**
|
|
17
|
+
* Field used to match OAuth profiles to existing users (by the profile's email).
|
|
18
|
+
* @default 'email'
|
|
19
|
+
*/
|
|
20
|
+
identityField?: string
|
|
21
|
+
/**
|
|
22
|
+
* Create users on first sign-in. When false, sign-ins from unknown
|
|
23
|
+
* profiles are redirected to `failureRedirect`.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
allowSignup?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the data for a user being created on first sign-in.
|
|
29
|
+
* Return null to reject the sign-in. Defaults to `{ [identityField]: profile.email }`
|
|
30
|
+
* plus `name` when the collection's input accepts it.
|
|
31
|
+
*/
|
|
32
|
+
resolveUser?: (args: {
|
|
33
|
+
provider: string
|
|
34
|
+
profile: OAuthProfile
|
|
35
|
+
tokens: OAuthTokens
|
|
36
|
+
context: NixxieContext
|
|
37
|
+
}) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null
|
|
38
|
+
/**
|
|
39
|
+
* Called after a successful sign-in (e.g. to link accounts or audit). Receives the
|
|
40
|
+
* signed-in user's id.
|
|
41
|
+
*/
|
|
42
|
+
onSignIn?: (args: {
|
|
43
|
+
provider: string
|
|
44
|
+
itemId: string
|
|
45
|
+
created: boolean
|
|
46
|
+
profile: OAuthProfile
|
|
47
|
+
tokens: OAuthTokens
|
|
48
|
+
context: NixxieContext
|
|
49
|
+
}) => Promise<void> | void
|
|
50
|
+
/**
|
|
51
|
+
* Base path the routes are mounted under: `<basePath>/:provider` starts the flow and
|
|
52
|
+
* `<basePath>/:provider/callback` completes it.
|
|
53
|
+
* @default '/auth'
|
|
54
|
+
*/
|
|
55
|
+
basePath?: string
|
|
56
|
+
/** Where to send the user after a successful sign-in. @default '/' */
|
|
57
|
+
successRedirect?: string
|
|
58
|
+
/** Where to send the user after a failure (gets `?error=<code>` appended). @default '/signin' */
|
|
59
|
+
failureRedirect?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseCookies(header: string | undefined): Record<string, string> {
|
|
63
|
+
const out: Record<string, string> = {}
|
|
64
|
+
if (!header) return out
|
|
65
|
+
for (const part of header.split(';')) {
|
|
66
|
+
const eq = part.indexOf('=')
|
|
67
|
+
if (eq === -1) continue
|
|
68
|
+
out[part.slice(0, eq).trim()] = decodeURIComponent(part.slice(eq + 1).trim())
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mount ready-made OAuth sign-in routes on the Express app:
|
|
75
|
+
*
|
|
76
|
+
* - `GET <basePath>/:provider` — redirects to the provider's consent screen
|
|
77
|
+
* - `GET <basePath>/:provider/callback` — exchanges the code, finds-or-creates the
|
|
78
|
+
* user, starts a session (compatible with `createAuth` sessions: `{ itemId }`),
|
|
79
|
+
* and redirects
|
|
80
|
+
*
|
|
81
|
+
* Call from `server.extendExpressApp`, or use `oauthPlugin()` to wire it automatically.
|
|
82
|
+
*/
|
|
83
|
+
export function installOAuthRoutes(
|
|
84
|
+
app: any,
|
|
85
|
+
context: NixxieContext,
|
|
86
|
+
options: OAuthRoutesOptions
|
|
87
|
+
): void {
|
|
88
|
+
const {
|
|
89
|
+
oauth,
|
|
90
|
+
listKey = 'User',
|
|
91
|
+
identityField = 'email',
|
|
92
|
+
allowSignup = true,
|
|
93
|
+
resolveUser,
|
|
94
|
+
onSignIn,
|
|
95
|
+
basePath = '/auth',
|
|
96
|
+
successRedirect = '/',
|
|
97
|
+
failureRedirect = '/signin',
|
|
98
|
+
} = options
|
|
99
|
+
|
|
100
|
+
// The state value is signed so the callback can verify it even though the cookie
|
|
101
|
+
// itself is the only storage — no server-side flow state needed.
|
|
102
|
+
const stateSecret = randomBytes(32).toString('hex')
|
|
103
|
+
const signState = (value: string) =>
|
|
104
|
+
`${value}.${createHmac('sha256', stateSecret).update(value).digest('hex')}`
|
|
105
|
+
const verifyState = (signed: string | undefined): string | undefined => {
|
|
106
|
+
if (!signed) return
|
|
107
|
+
const at = signed.lastIndexOf('.')
|
|
108
|
+
if (at === -1) return
|
|
109
|
+
const value = signed.slice(0, at)
|
|
110
|
+
const mac = createHmac('sha256', stateSecret).update(value).digest('hex')
|
|
111
|
+
return mac === signed.slice(at + 1) ? value : undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fail = (res: any, code: string) => {
|
|
115
|
+
const sep = failureRedirect.includes('?') ? '&' : '?'
|
|
116
|
+
res.redirect(`${failureRedirect}${sep}error=${encodeURIComponent(code)}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
app.get(`${basePath}/:provider`, (req: any, res: any) => {
|
|
120
|
+
try {
|
|
121
|
+
const provider = req.params.provider
|
|
122
|
+
if (!oauth.list().includes(provider)) return fail(res, 'unknown-provider')
|
|
123
|
+
|
|
124
|
+
const state = oauth.createState()
|
|
125
|
+
res.cookie?.(STATE_COOKIE, signState(`${provider}:${state}`), {
|
|
126
|
+
httpOnly: true,
|
|
127
|
+
sameSite: 'lax',
|
|
128
|
+
secure: process.env.NODE_ENV === 'production',
|
|
129
|
+
maxAge: 10 * 60 * 1000,
|
|
130
|
+
path: basePath,
|
|
131
|
+
})
|
|
132
|
+
res.redirect(oauth.authorizationUrl(provider, { state }))
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('[nixxie/auth-oauth] start failed:', err)
|
|
135
|
+
fail(res, 'oauth-start-failed')
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
app.get(`${basePath}/:provider/callback`, (req: any, res: any) => {
|
|
140
|
+
void (async () => {
|
|
141
|
+
const provider = req.params.provider
|
|
142
|
+
const { code, state, error } = req.query ?? {}
|
|
143
|
+
if (error) return fail(res, String(error))
|
|
144
|
+
if (!code) return fail(res, 'missing-code')
|
|
145
|
+
|
|
146
|
+
const stored = verifyState(parseCookies(req.headers.cookie)[STATE_COOKIE])
|
|
147
|
+
res.clearCookie?.(STATE_COOKIE, { path: basePath })
|
|
148
|
+
if (!stored || stored !== `${provider}:${state}`) return fail(res, 'invalid-state')
|
|
149
|
+
|
|
150
|
+
const { tokens, profile } = await oauth.callback(provider, String(code))
|
|
151
|
+
if (!profile.email) return fail(res, 'no-email')
|
|
152
|
+
|
|
153
|
+
const requestContext = await context.withRequest(req, res)
|
|
154
|
+
const sudo = requestContext.sudo()
|
|
155
|
+
|
|
156
|
+
const existing = await sudo.db[listKey].findMany({
|
|
157
|
+
where: { [identityField]: { equals: profile.email } } as any,
|
|
158
|
+
take: 1,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
let itemId: string
|
|
162
|
+
let created = false
|
|
163
|
+
if (existing.length) {
|
|
164
|
+
itemId = String(existing[0].id)
|
|
165
|
+
} else {
|
|
166
|
+
if (!allowSignup) return fail(res, 'unknown-user')
|
|
167
|
+
const data = resolveUser
|
|
168
|
+
? await resolveUser({ provider, profile, tokens, context: requestContext })
|
|
169
|
+
: { [identityField]: profile.email }
|
|
170
|
+
if (!data) return fail(res, 'signup-rejected')
|
|
171
|
+
const createdItem = await sudo.db[listKey].createOne({ data: data as any })
|
|
172
|
+
itemId = String(createdItem.id)
|
|
173
|
+
created = true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!requestContext.sessionStrategy) {
|
|
177
|
+
throw new Error('installOAuthRoutes: no session strategy configured')
|
|
178
|
+
}
|
|
179
|
+
await requestContext.sessionStrategy.start({
|
|
180
|
+
context: requestContext,
|
|
181
|
+
data: { itemId } as any,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
await onSignIn?.({ provider, itemId, created, profile, tokens, context: requestContext })
|
|
185
|
+
res.redirect(successRedirect)
|
|
186
|
+
})().catch((err: unknown) => {
|
|
187
|
+
console.error('[nixxie/auth-oauth] callback failed:', err)
|
|
188
|
+
fail(res, 'oauth-failed')
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The same OAuth routes as `installOAuthRoutes`, packaged as a Nixxie plugin.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* config({
|
|
198
|
+
* plugins: [
|
|
199
|
+
* oauthPlugin({
|
|
200
|
+
* oauth: createOAuth({ providers: { google: { provider: 'google', ... } } }),
|
|
201
|
+
* listKey: 'User',
|
|
202
|
+
* }),
|
|
203
|
+
* ],
|
|
204
|
+
* })
|
|
205
|
+
*/
|
|
206
|
+
export function oauthPlugin(options: OAuthRoutesOptions): NixxiePlugin {
|
|
207
|
+
return {
|
|
208
|
+
name: 'nixxie-auth-oauth',
|
|
209
|
+
extendConfig: config => ({
|
|
210
|
+
...config,
|
|
211
|
+
server: {
|
|
212
|
+
...config.server,
|
|
213
|
+
extendExpressApp: async (app: any, context: NixxieContext) => {
|
|
214
|
+
await (config.server as any)?.extendExpressApp?.(app, context)
|
|
215
|
+
installOAuthRoutes(app, context, options)
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export function createOAuth(config: OAuthConfig): OAuth {
|
|
|
110
110
|
return new OAuth(config)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export { installOAuthRoutes, oauthPlugin } from './express'
|
|
114
|
+
export type { OAuthRoutesOptions } from './express'
|
|
113
115
|
export { builtInProviders as providers }
|
|
114
116
|
export type {
|
|
115
117
|
OAuthConfig,
|