@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 +9 -0
- package/dist/cjs/core/errors.d.ts +1 -1
- package/dist/cjs/core/types.d.ts +8 -0
- package/dist/cjs/core/types.js +20 -0
- package/dist/cjs/nest/bastion-jwks.service.d.ts +8 -1
- package/dist/cjs/nest/bastion-jwks.service.js +19 -0
- package/dist/esm/core/errors.d.ts +1 -1
- package/dist/esm/core/types.d.ts +8 -0
- package/dist/esm/core/types.js +19 -0
- package/dist/esm/nest/bastion-jwks.service.d.ts +8 -1
- package/dist/esm/nest/bastion-jwks.service.js +19 -0
- package/package.json +1 -1
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;
|
package/dist/cjs/core/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/cjs/core/types.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/esm/core/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/esm/core/types.js
CHANGED
|
@@ -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
|
|
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.
|
|
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)",
|