@tsed/passport 8.44.0 → 8.44.1

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/lib/esm/index.js CHANGED
@@ -20,3 +20,4 @@ export * from "./PassportModule.js";
20
20
  export * from "./services/PassportSerializerService.js";
21
21
  export * from "./services/ProtocolsService.js";
22
22
  export * from "./utils/getProtocolsFromRequest.js";
23
+ export * from "./utils/sessionProtocol.js";
@@ -1,29 +1,58 @@
1
1
  var PassportMiddleware_1;
2
2
  import { __decorate, __metadata, __param } from "tslib";
3
+ import { isArray } from "@tsed/core";
3
4
  import { Context } from "@tsed/platform-params";
4
5
  import { Inject } from "@tsed/di";
5
6
  import { Middleware } from "@tsed/platform-middlewares";
6
7
  import { ProtocolsService } from "../services/ProtocolsService.js";
7
8
  import { getProtocolsFromRequest } from "../utils/getProtocolsFromRequest.js";
9
+ import { getSessionProtocol, hasLoginSession } from "../utils/sessionProtocol.js";
10
+ /**
11
+ * Reserved protocol name satisfied by an existing login session. It never runs a Passport strategy.
12
+ */
13
+ export const SESSION_PROTOCOL = "session";
8
14
  let PassportMiddleware = PassportMiddleware_1 = class PassportMiddleware {
9
- shouldSkip(ctx) {
15
+ /**
16
+ * Return true when an existing login session satisfies the protocols declared on the endpoint:
17
+ *
18
+ * - the endpoint accepts any protocol (`"*"`),
19
+ * - the endpoint accepts the reserved `"session"` protocol and the request carries a persisted login session,
20
+ * - or the persisted login session was established by one of the endpoint protocols.
21
+ *
22
+ * A session established by another protocol never satisfies the endpoint: its strategies run instead.
23
+ */
24
+ shouldSkip(protocol, protocols, ctx) {
10
25
  const request = ctx.getRequest();
11
- return request.user && request.isAuthenticated();
26
+ if (!(request.user && request.isAuthenticated?.())) {
27
+ return false;
28
+ }
29
+ const acceptsAny = isArray(protocol) ? protocol.includes("*") : protocol === "*";
30
+ if (acceptsAny) {
31
+ return true;
32
+ }
33
+ // The session rules require a persisted login session, not only a user set by a stateless strategy.
34
+ if (!hasLoginSession(request)) {
35
+ return false;
36
+ }
37
+ const sessionProtocol = getSessionProtocol(request);
38
+ return protocols.includes(SESSION_PROTOCOL) || (!!sessionProtocol && protocols.includes(sessionProtocol));
12
39
  }
13
40
  async use(ctx) {
14
41
  const endpoint = ctx.endpoint;
15
42
  const request = ctx.getRequest();
16
- if (this.shouldSkip(ctx)) {
17
- return;
18
- }
19
43
  const { options, protocol, method, originalUrl = true } = endpoint.store.get(PassportMiddleware_1);
20
44
  const protocols = getProtocolsFromRequest(request, protocol, this.protocolsService.getProtocolsNames());
45
+ if (this.shouldSkip(protocol, protocols, ctx)) {
46
+ return;
47
+ }
48
+ // The session protocol has been checked above: it never runs a Passport strategy.
49
+ const strategies = protocols.filter((name) => name !== SESSION_PROTOCOL);
21
50
  if (originalUrl) {
22
51
  request.url = request.originalUrl;
23
52
  }
24
53
  await (method === "authenticate"
25
- ? this.protocolsService.authenticate(protocols, options, ctx)
26
- : this.protocolsService.authorize(protocols, options, ctx));
54
+ ? this.protocolsService.authenticate(strategies, options, ctx)
55
+ : this.protocolsService.authorize(strategies, options, ctx));
27
56
  }
28
57
  };
29
58
  __decorate([
@@ -7,12 +7,19 @@ import { PassportMessage } from "../errors/PassportMessage.js";
7
7
  import { Unauthorized } from "@tsed/exceptions";
8
8
  import { ancestorsOf } from "@tsed/core";
9
9
  import { promisify } from "node:util";
10
+ import { setSessionProtocol } from "../utils/sessionProtocol.js";
11
+ /**
12
+ * Context key holding the name of the protocol whose verify callback accepted the request.
13
+ * @ignore
14
+ */
15
+ const VERIFIED_PROTOCOL = "PASSPORT_VERIFIED_PROTOCOL";
10
16
  /**
11
17
  * @ignore
12
18
  */
13
19
  export class ProtocolsService {
14
20
  constructor() {
15
21
  this.strategies = new Map();
22
+ this.loginOnlyProtocols = new Set();
16
23
  this.platformHandler = inject(PlatformHandler);
17
24
  }
18
25
  getProtocols() {
@@ -21,18 +28,31 @@ export class ProtocolsService {
21
28
  getProtocolsNames() {
22
29
  return Array.from(this.strategies.keys());
23
30
  }
31
+ /**
32
+ * Return true when the protocol must only run on login endpoints (`@Authenticate()`).
33
+ * @param name
34
+ */
35
+ isLoginOnly(name) {
36
+ return this.loginOnlyProtocols.has(name);
37
+ }
24
38
  /**
25
39
  * Invoke provider and bind it to passport.
26
40
  * @param provider
27
41
  */
28
42
  async invoke(provider) {
29
- let { name, useStrategy: strategy, settings } = this.getOptions(provider);
43
+ let { name, useStrategy: strategy, settings, loginOnly } = this.getOptions(provider);
30
44
  const protocol = inject(provider.token);
31
45
  if (protocol.$beforeInstall) {
32
46
  settings = (await protocol.$beforeInstall(settings)) || settings;
33
47
  }
34
48
  const instance = new strategy(settings, this.createHandler(provider));
35
49
  this.strategies.set(name, instance);
50
+ if (loginOnly) {
51
+ this.loginOnlyProtocols.add(name);
52
+ }
53
+ else {
54
+ this.loginOnlyProtocols.delete(name);
55
+ }
36
56
  Passport.use(name, instance);
37
57
  if (protocol.$onInstall) {
38
58
  await protocol.$onInstall(instance);
@@ -56,7 +76,9 @@ export class ProtocolsService {
56
76
  * @param ctx
57
77
  */
58
78
  authorize(protocols, options, ctx) {
59
- return this.call("authorize", protocols, options, ctx);
79
+ // Login-only protocols never run on a guarded route: they are accepted through their session only.
80
+ const strategies = [].concat(protocols).filter((name) => !this.isLoginOnly(name));
81
+ return this.call("authorize", strategies, options, ctx);
60
82
  }
61
83
  /**
62
84
  * Call passport authenticate or authorize depending on the chosen method.
@@ -73,6 +95,7 @@ export class ProtocolsService {
73
95
  if (protocols.length === 0) {
74
96
  throw new Unauthorized("Not authorized");
75
97
  }
98
+ const restoreLogIn = method === "authenticate" ? this.recordSessionProtocol(protocols, ctx) : undefined;
76
99
  try {
77
100
  options.failWithError = true;
78
101
  // @ts-ignore
@@ -85,6 +108,41 @@ export class ProtocolsService {
85
108
  }
86
109
  throw er;
87
110
  }
111
+ finally {
112
+ restoreLogIn?.();
113
+ }
114
+ }
115
+ /**
116
+ * Wrap `req.logIn()` for the running `@Authenticate()` call to record the protocol that establishes the
117
+ * login session. The record is written in the `req.logIn()` callback, once Passport.js has finished updating
118
+ * the session (`keepSessionInfo` merge, Passport.js 0.4 session object replacement) and before it ends the
119
+ * request itself (`successRedirect`).
120
+ *
121
+ * Return a function restoring the original `req.logIn()`.
122
+ */
123
+ recordSessionProtocol(protocols, ctx) {
124
+ const request = ctx.getRequest();
125
+ const logIn = request.logIn;
126
+ if (typeof logIn !== "function") {
127
+ return;
128
+ }
129
+ request.login = request.logIn = function (user, options, done) {
130
+ if (typeof options === "function") {
131
+ done = options;
132
+ options = {};
133
+ }
134
+ return logIn.call(this, user, options, (err) => {
135
+ const verified = ctx.get(VERIFIED_PROTOCOL);
136
+ const protocol = protocols.includes(verified) ? verified : protocols.length === 1 ? protocols[0] : undefined;
137
+ if (!err && options?.session !== false && protocol) {
138
+ setSessionProtocol(this, protocol);
139
+ }
140
+ done?.(err);
141
+ });
142
+ };
143
+ return () => {
144
+ request.login = request.logIn = logIn;
145
+ };
88
146
  }
89
147
  /**
90
148
  * Create strategy options based on decorator metadata and global configuration
@@ -92,11 +150,12 @@ export class ProtocolsService {
92
150
  * @private
93
151
  */
94
152
  getOptions(provider) {
95
- const { name } = provider.store.get("protocol");
96
- const { useStrategy = Strategy, settings = {} } = constant(`passport.protocols.${name}`) || {};
153
+ const { name, ...metadata } = provider.store.get("protocol");
154
+ const { useStrategy = Strategy, settings = {}, loginOnly = metadata.loginOnly ?? false } = constant(`passport.protocols.${name}`) ?? {};
97
155
  return {
98
156
  name,
99
157
  useStrategy,
158
+ loginOnly,
100
159
  settings: {
101
160
  ...settings,
102
161
  passReqToCallback: true
@@ -110,13 +169,18 @@ export class ProtocolsService {
110
169
  */
111
170
  createHandler(provider) {
112
171
  const middleware = this.platformHandler.createCustomHandler(provider, "$onVerify");
172
+ const { name } = provider.store.get("protocol");
113
173
  return async (req, ...args) => {
114
174
  const done = args[args.length - 1];
115
175
  if (req.$ctx) {
116
176
  req.$ctx.set("PROTOCOL_ARGS", args.slice(0, -1));
117
177
  try {
118
178
  await middleware(req.$ctx);
119
- done(null, ...[].concat(req.$ctx.data));
179
+ const results = [].concat(req.$ctx.data);
180
+ if (results[0]) {
181
+ req.$ctx.set(VERIFIED_PROTOCOL, name);
182
+ }
183
+ done(null, ...results);
120
184
  }
121
185
  catch (err) {
122
186
  const error = err;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Session key used by Passport.js to store the login state (`req.session.passport.user`).
3
+ * @ignore
4
+ */
5
+ const PASSPORT_SESSION_KEY = "passport";
6
+ /**
7
+ * Return true when the request carries a login session persisted by Passport.js. `req.isAuthenticated()` is not
8
+ * enough: a stateless strategy (`session: false`) sets `req.user` without any session.
9
+ * @ignore
10
+ */
11
+ export function hasLoginSession(req) {
12
+ return req.session?.[PASSPORT_SESSION_KEY]?.user !== undefined;
13
+ }
14
+ /**
15
+ * Return the name of the protocol that established the current login session.
16
+ * @ignore
17
+ */
18
+ export function getSessionProtocol(req) {
19
+ return req.session?.[PASSPORT_SESSION_KEY]?.protocol;
20
+ }
21
+ /**
22
+ * Record the protocol that established the current login session, next to the user stored by Passport.js.
23
+ * The record is dropped with the login state on `req.logout()`.
24
+ * @ignore
25
+ */
26
+ export function setSessionProtocol(req, protocol) {
27
+ const state = req.session?.[PASSPORT_SESSION_KEY];
28
+ if (state && state.user !== undefined) {
29
+ state.protocol = protocol;
30
+ }
31
+ }
@@ -20,3 +20,4 @@ export * from "./PassportModule.js";
20
20
  export * from "./services/PassportSerializerService.js";
21
21
  export * from "./services/ProtocolsService.js";
22
22
  export * from "./utils/getProtocolsFromRequest.js";
23
+ export * from "./utils/sessionProtocol.js";
@@ -4,4 +4,14 @@ export interface ProtocolOptions<Settings = any> {
4
4
  name: string;
5
5
  useStrategy: Type<Strategy>;
6
6
  settings: Settings;
7
+ /**
8
+ * Run the strategy only on login endpoints decorated with `@Authenticate()`.
9
+ *
10
+ * A route decorated with `@Authorize()` then accepts this protocol only through the session it established,
11
+ * and never runs the strategy itself. Use it for interactive login strategies (`passport-local`, OAuth, ...)
12
+ * so that credentials sent to a business route do not open a new login flow.
13
+ *
14
+ * @default false
15
+ */
16
+ loginOnly?: boolean;
7
17
  }
@@ -1,7 +1,20 @@
1
1
  import { Context } from "@tsed/platform-params";
2
2
  import { ProtocolsService } from "../services/ProtocolsService.js";
3
+ /**
4
+ * Reserved protocol name satisfied by an existing login session. It never runs a Passport strategy.
5
+ */
6
+ export declare const SESSION_PROTOCOL = "session";
3
7
  export declare class PassportMiddleware {
4
8
  protocolsService: ProtocolsService;
5
- shouldSkip(ctx: Context): any;
9
+ /**
10
+ * Return true when an existing login session satisfies the protocols declared on the endpoint:
11
+ *
12
+ * - the endpoint accepts any protocol (`"*"`),
13
+ * - the endpoint accepts the reserved `"session"` protocol and the request carries a persisted login session,
14
+ * - or the persisted login session was established by one of the endpoint protocols.
15
+ *
16
+ * A session established by another protocol never satisfies the endpoint: its strategies run instead.
17
+ */
18
+ shouldSkip(protocol: string | string[], protocols: string[], ctx: Context): boolean;
6
19
  use(ctx: Context): Promise<void>;
7
20
  }
@@ -7,9 +7,15 @@ import type { ProtocolMethods } from "../interfaces/ProtocolMethods.js";
7
7
  */
8
8
  export declare class ProtocolsService {
9
9
  readonly strategies: Map<string, Strategy>;
10
+ readonly loginOnlyProtocols: Set<string>;
10
11
  protected platformHandler: PlatformHandler;
11
12
  getProtocols(): Provider[];
12
13
  getProtocolsNames(): string[];
14
+ /**
15
+ * Return true when the protocol must only run on login endpoints (`@Authenticate()`).
16
+ * @param name
17
+ */
18
+ isLoginOnly(name: string): boolean;
13
19
  /**
14
20
  * Invoke provider and bind it to passport.
15
21
  * @param provider
@@ -38,6 +44,15 @@ export declare class ProtocolsService {
38
44
  * @private
39
45
  */
40
46
  private call;
47
+ /**
48
+ * Wrap `req.logIn()` for the running `@Authenticate()` call to record the protocol that establishes the
49
+ * login session. The record is written in the `req.logIn()` callback, once Passport.js has finished updating
50
+ * the session (`keepSessionInfo` merge, Passport.js 0.4 session object replacement) and before it ends the
51
+ * request itself (`successRedirect`).
52
+ *
53
+ * Return a function restoring the original `req.logIn()`.
54
+ */
55
+ private recordSessionProtocol;
41
56
  /**
42
57
  * Create strategy options based on decorator metadata and global configuration
43
58
  * @param provider
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Return true when the request carries a login session persisted by Passport.js. `req.isAuthenticated()` is not
3
+ * enough: a stateless strategy (`session: false`) sets `req.user` without any session.
4
+ * @ignore
5
+ */
6
+ export declare function hasLoginSession(req: any): boolean;
7
+ /**
8
+ * Return the name of the protocol that established the current login session.
9
+ * @ignore
10
+ */
11
+ export declare function getSessionProtocol(req: any): string | undefined;
12
+ /**
13
+ * Record the protocol that established the current login session, next to the user stored by Passport.js.
14
+ * The record is dropped with the login state on `req.logout()`.
15
+ * @ignore
16
+ */
17
+ export declare function setSessionProtocol(req: any, protocol: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/passport",
3
- "version": "8.44.0",
3
+ "version": "8.44.1",
4
4
  "description": "Passport package for Ts.ED framework",
5
5
  "source": "./src/index.ts",
6
6
  "type": "module",
@@ -28,12 +28,12 @@
28
28
  "tslib": ">=2.7.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@tsed/barrels": "8.44.0",
32
- "@tsed/core": "8.44.0",
33
- "@tsed/di": "8.44.0",
34
- "@tsed/platform-http": "8.44.0",
35
- "@tsed/platform-router": "8.44.0",
36
- "@tsed/typescript": "8.44.0",
31
+ "@tsed/barrels": "8.44.1",
32
+ "@tsed/core": "8.44.1",
33
+ "@tsed/di": "8.44.1",
34
+ "@tsed/platform-http": "8.44.1",
35
+ "@tsed/platform-router": "8.44.1",
36
+ "@tsed/typescript": "8.44.1",
37
37
  "@types/passport": "1.0.16",
38
38
  "@types/passport-http": "0.3.11",
39
39
  "@types/passport-local": "1.0.38",