@heyatom/bastion-client 0.1.2 → 0.2.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 CHANGED
@@ -62,6 +62,15 @@ export class AdminController {
62
62
 
63
63
  Bastion stores app events as `<serviceSlug>.<event>` — send the bare `resource.action`.
64
64
 
65
+ ## App backends: verifying your users' tokens
66
+
67
+ ```ts
68
+ // BastionJwksService.verifyUserToken(token): a user session token minted for this app
69
+ // (rejects machine tokens, checks aud + appSlug === serviceSlug, requires sub). Build your
70
+ // own guard on it — cookie extraction, suspension, age gate are the app's business.
71
+ // Core equivalent: assertUserTokenFor(await verifier.verify(token), 'dbd-builds').
72
+ ```
73
+
65
74
  ## Without NestJS
66
75
 
67
76
  ```ts
@@ -1,4 +1,4 @@
1
- export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid';
1
+ export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid' | 'not_user_token' | 'wrong_app';
2
2
  /** Verification failure. `code` is safe to log; the message is safe to return to the caller. */
3
3
  export declare class BastionTokenError extends Error {
4
4
  readonly code: TokenErrorCode;
@@ -29,6 +29,14 @@ export interface ServiceClientJwtPayload extends CommonClaims {
29
29
  scopes: string[];
30
30
  }
31
31
  export type BastionJwtPayload = UserJwtPayload | ServiceClientJwtPayload;
32
+ /**
33
+ * For an app backend that accepts its *users'* tokens: the payload must be a
34
+ * user token (machine tokens are rejected whatever their scopes), minted for
35
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
36
+ * one signing key serves the whole fleet, so a valid signature alone proves
37
+ * nothing about which app the session belongs to.
38
+ */
39
+ export declare function assertUserTokenFor(payload: BastionJwtPayload, appSlug: string): UserJwtPayload;
32
40
  export declare function isServiceClientToken(payload: BastionJwtPayload): payload is ServiceClientJwtPayload;
33
41
  export interface TokenResponse {
34
42
  accessToken: string;
@@ -1,7 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.silentLogger = void 0;
4
+ exports.assertUserTokenFor = assertUserTokenFor;
4
5
  exports.isServiceClientToken = isServiceClientToken;
6
+ const errors_js_1 = require("./errors.js");
7
+ /**
8
+ * For an app backend that accepts its *users'* tokens: the payload must be a
9
+ * user token (machine tokens are rejected whatever their scopes), minted for
10
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
11
+ * one signing key serves the whole fleet, so a valid signature alone proves
12
+ * nothing about which app the session belongs to.
13
+ */
14
+ function assertUserTokenFor(payload, appSlug) {
15
+ if (payload.type !== undefined) {
16
+ throw new errors_js_1.BastionTokenError('not_user_token', 'Service token not allowed');
17
+ }
18
+ if (payload.aud !== appSlug || payload.appSlug !== appSlug) {
19
+ throw new errors_js_1.BastionTokenError('wrong_app', 'Invalid app context');
20
+ }
21
+ if (!payload.sub)
22
+ throw new errors_js_1.BastionTokenError('wrong_app', 'Token without subject');
23
+ return payload;
24
+ }
5
25
  function isServiceClientToken(payload) {
6
26
  return payload.type === 'service_client';
7
27
  }
@@ -1,8 +1,15 @@
1
- import type { BastionJwtPayload } from '../core/types.js';
1
+ import { type BastionJwtPayload, type UserJwtPayload } from '../core/types.js';
2
2
  import { type BastionModuleOptions } from './options.js';
3
3
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
4
4
  export declare class BastionJwksService {
5
5
  private readonly verifier;
6
+ private readonly appSlug;
6
7
  constructor(options: BastionModuleOptions);
7
8
  verify(token: string): Promise<BastionJwtPayload>;
9
+ /**
10
+ * A *user* session token issued for this app (`options.serviceSlug` is the
11
+ * app's own slug). What an app backend puts behind its own auth guard —
12
+ * cookie extraction, suspension, age gate stay the app's business.
13
+ */
14
+ verifyUserToken(token: string): Promise<UserJwtPayload>;
8
15
  }
@@ -17,12 +17,15 @@ exports.BastionJwksService = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const jwks_verifier_js_1 = require("../core/jwks-verifier.js");
19
19
  const errors_js_1 = require("../core/errors.js");
20
+ const types_js_1 = require("../core/types.js");
20
21
  const options_js_1 = require("./options.js");
21
22
  const nest_logger_js_1 = require("./nest-logger.js");
22
23
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
23
24
  let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
24
25
  verifier;
26
+ appSlug;
25
27
  constructor(options) {
28
+ this.appSlug = options.serviceSlug;
26
29
  this.verifier = new jwks_verifier_js_1.JwksVerifier({
27
30
  baseUrl: options.baseUrl,
28
31
  ttlMs: options.jwksTtlMs,
@@ -40,6 +43,22 @@ let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
40
43
  throw err;
41
44
  }
42
45
  }
46
+ /**
47
+ * A *user* session token issued for this app (`options.serviceSlug` is the
48
+ * app's own slug). What an app backend puts behind its own auth guard —
49
+ * cookie extraction, suspension, age gate stay the app's business.
50
+ */
51
+ async verifyUserToken(token) {
52
+ const payload = await this.verify(token);
53
+ try {
54
+ return (0, types_js_1.assertUserTokenFor)(payload, this.appSlug);
55
+ }
56
+ catch (err) {
57
+ if (err instanceof errors_js_1.BastionTokenError)
58
+ throw new common_1.UnauthorizedException(err.message);
59
+ throw err;
60
+ }
61
+ }
43
62
  };
44
63
  exports.BastionJwksService = BastionJwksService;
45
64
  exports.BastionJwksService = BastionJwksService = BastionJwksService_1 = __decorate([
@@ -1,4 +1,4 @@
1
- export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid';
1
+ export type TokenErrorCode = 'malformed' | 'missing_kid' | 'unknown_kid' | 'invalid' | 'not_user_token' | 'wrong_app';
2
2
  /** Verification failure. `code` is safe to log; the message is safe to return to the caller. */
3
3
  export declare class BastionTokenError extends Error {
4
4
  readonly code: TokenErrorCode;
@@ -29,6 +29,14 @@ export interface ServiceClientJwtPayload extends CommonClaims {
29
29
  scopes: string[];
30
30
  }
31
31
  export type BastionJwtPayload = UserJwtPayload | ServiceClientJwtPayload;
32
+ /**
33
+ * For an app backend that accepts its *users'* tokens: the payload must be a
34
+ * user token (machine tokens are rejected whatever their scopes), minted for
35
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
36
+ * one signing key serves the whole fleet, so a valid signature alone proves
37
+ * nothing about which app the session belongs to.
38
+ */
39
+ export declare function assertUserTokenFor(payload: BastionJwtPayload, appSlug: string): UserJwtPayload;
32
40
  export declare function isServiceClientToken(payload: BastionJwtPayload): payload is ServiceClientJwtPayload;
33
41
  export interface TokenResponse {
34
42
  accessToken: string;
@@ -1,3 +1,22 @@
1
+ import { BastionTokenError } from './errors.js';
2
+ /**
3
+ * For an app backend that accepts its *users'* tokens: the payload must be a
4
+ * user token (machine tokens are rejected whatever their scopes), minted for
5
+ * this app — Bastion sets `aud` and `appSlug` to the app slug at issuance, and
6
+ * one signing key serves the whole fleet, so a valid signature alone proves
7
+ * nothing about which app the session belongs to.
8
+ */
9
+ export function assertUserTokenFor(payload, appSlug) {
10
+ if (payload.type !== undefined) {
11
+ throw new BastionTokenError('not_user_token', 'Service token not allowed');
12
+ }
13
+ if (payload.aud !== appSlug || payload.appSlug !== appSlug) {
14
+ throw new BastionTokenError('wrong_app', 'Invalid app context');
15
+ }
16
+ if (!payload.sub)
17
+ throw new BastionTokenError('wrong_app', 'Token without subject');
18
+ return payload;
19
+ }
1
20
  export function isServiceClientToken(payload) {
2
21
  return payload.type === 'service_client';
3
22
  }
@@ -1,8 +1,15 @@
1
- import type { BastionJwtPayload } from '../core/types.js';
1
+ import { type BastionJwtPayload, type UserJwtPayload } from '../core/types.js';
2
2
  import { type BastionModuleOptions } from './options.js';
3
3
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
4
4
  export declare class BastionJwksService {
5
5
  private readonly verifier;
6
+ private readonly appSlug;
6
7
  constructor(options: BastionModuleOptions);
7
8
  verify(token: string): Promise<BastionJwtPayload>;
9
+ /**
10
+ * A *user* session token issued for this app (`options.serviceSlug` is the
11
+ * app's own slug). What an app backend puts behind its own auth guard —
12
+ * cookie extraction, suspension, age gate stay the app's business.
13
+ */
14
+ verifyUserToken(token: string): Promise<UserJwtPayload>;
8
15
  }
@@ -14,12 +14,15 @@ var BastionJwksService_1;
14
14
  import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
15
15
  import { JwksVerifier } from '../core/jwks-verifier.js';
16
16
  import { BastionTokenError } from '../core/errors.js';
17
+ import { assertUserTokenFor } from '../core/types.js';
17
18
  import { BASTION_OPTIONS } from './options.js';
18
19
  import { nestLogger } from './nest-logger.js';
19
20
  /** Verifies inbound Bastion tokens against the cached JWKS. Throws `UnauthorizedException` on any failure. */
20
21
  let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
21
22
  verifier;
23
+ appSlug;
22
24
  constructor(options) {
25
+ this.appSlug = options.serviceSlug;
23
26
  this.verifier = new JwksVerifier({
24
27
  baseUrl: options.baseUrl,
25
28
  ttlMs: options.jwksTtlMs,
@@ -37,6 +40,22 @@ let BastionJwksService = BastionJwksService_1 = class BastionJwksService {
37
40
  throw err;
38
41
  }
39
42
  }
43
+ /**
44
+ * A *user* session token issued for this app (`options.serviceSlug` is the
45
+ * app's own slug). What an app backend puts behind its own auth guard —
46
+ * cookie extraction, suspension, age gate stay the app's business.
47
+ */
48
+ async verifyUserToken(token) {
49
+ const payload = await this.verify(token);
50
+ try {
51
+ return assertUserTokenFor(payload, this.appSlug);
52
+ }
53
+ catch (err) {
54
+ if (err instanceof BastionTokenError)
55
+ throw new UnauthorizedException(err.message);
56
+ throw err;
57
+ }
58
+ }
40
59
  };
41
60
  BastionJwksService = BastionJwksService_1 = __decorate([
42
61
  Injectable(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyatom/bastion-client",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Consumer SDK for the Bastion identity provider: JWKS verification, service-client auth, audit writes, NestJS guards",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Tombolato <hey@heyatom.dev> (https://heyatom.dev)",