@guardian/pan-domain-node 0.5.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/CHANGELOG.md +118 -0
- package/CODEOWNERS +1 -1
- package/README.md +65 -14
- package/dist/src/api.d.ts +31 -10
- package/dist/src/api.js +14 -9
- package/dist/src/fetch-public-key.d.ts +2 -1
- package/dist/src/fetch-public-key.js +48 -20
- package/dist/src/panda.d.ts +5 -3
- package/dist/src/panda.js +88 -29
- package/dist/src/utils.d.ts +6 -4
- package/dist/src/utils.js +52 -19
- package/dist/test/fixtures.js +2 -2
- package/dist/test/panda.test.js +199 -26
- package/dist/test/utils.test.js +10 -5
- package/package.json +9 -6
- package/src/api.ts +63 -10
- package/src/fetch-public-key.ts +29 -13
- package/src/panda.ts +68 -23
- package/src/utils.ts +30 -4
- package/test/panda.test.ts +227 -26
- package/test/utils.test.ts +12 -6
- package/.github/workflows/snyk.yml +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# @guardian/pan-domain-node
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b762261: Download public key from S3 with AWS credential
|
|
8
|
+
|
|
9
|
+
## 1.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- f91598a: # Changes
|
|
14
|
+
Adds a 24-hour grace period after cookie expiry, during which requests will still be considered authenticated.
|
|
15
|
+
|
|
16
|
+
This is modelled by changing `AuthenticatedStatus` into a discriminated union with the following properties (among others):
|
|
17
|
+
|
|
18
|
+
### `success`
|
|
19
|
+
|
|
20
|
+
Whether to treat request as authenticated or not. **This will remain true after cookie expiry for the length of the grace period.**
|
|
21
|
+
|
|
22
|
+
[Almost all consumers](https://docs.google.com/spreadsheets/d/19XABaP9ua935TYJARkL8tstnizL69gIJ9av8C3UQBYw/edit?gid=0#gid=0) of this library check **only** the `AUTHORISED` status right now. These should all be able to switch to checking just this single boolean, implicitly getting grace period functionality in the process.
|
|
23
|
+
|
|
24
|
+
### `shouldRefreshCredentials`
|
|
25
|
+
|
|
26
|
+
Whether to try and get fresh credentials.
|
|
27
|
+
|
|
28
|
+
This allows page endpoints to redirect to auth, and API endpoints to tell the frontend to show a warning message to the user.
|
|
29
|
+
|
|
30
|
+
### `mustRefreshByEpochTimeMillis`
|
|
31
|
+
|
|
32
|
+
The time at which the grace period ends and the request will be treated as unauthenticated. This allows library consumers to warn the user in the app UI when they are near the end of the grace period, as Composer does: https://github.com/guardian/flexible-content/pull/5210
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Panda cookie: issued expires `mustRefreshByEpochTimeMillis`
|
|
36
|
+
| | |
|
|
37
|
+
|--1 hour--| |
|
|
38
|
+
Grace period: [------------- 24 hours ------]
|
|
39
|
+
|
|
40
|
+
`success`: --false-][-true-----------------------------------][-false-------->
|
|
41
|
+
`shouldRefreshCredentials` [-false---][-true------------------------]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
# Why have we made this change?
|
|
45
|
+
|
|
46
|
+
The Panda authentication cookie expires after 1 hour, and top-level navigation requests (page loads) trigger automatic re-authentication after this point.
|
|
47
|
+
|
|
48
|
+
Unfortunately API requests cannot trigger re-authentication on their own, and background refresh mechanisms (e.g. iframe-based method used by [Pandular](https://github.com/guardian/pandular)) are increasingly blocked by browsers due to third-party cookie restrictions.
|
|
49
|
+
|
|
50
|
+
We would like to enforce a 24-hour grace period
|
|
51
|
+
|
|
52
|
+
# How to update consuming code
|
|
53
|
+
|
|
54
|
+
At a minimum, switch from
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const authResult = await panda.verify(cookieHeader);
|
|
58
|
+
if (
|
|
59
|
+
authResult.status === AuthenticationStatus.AUTHORISED &&
|
|
60
|
+
authResult.user
|
|
61
|
+
) {
|
|
62
|
+
return authResult.user.email;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
to
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const authResult = await panda.verify(cookieHeader);
|
|
70
|
+
if (authResult.success) {
|
|
71
|
+
return authResult.user.email;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This will implicitly give you grace period functionality, because `success` will remain true during the grace period.
|
|
76
|
+
|
|
77
|
+
However, we **strongly** recommend all consumers to take account of `shouldRefreshCredentials`. What you do with the result should depend on whether your endpoint can trigger re-auth.
|
|
78
|
+
|
|
79
|
+
## Endpoints that can refresh credentials
|
|
80
|
+
|
|
81
|
+
Endpoints that **can** refresh credentials, e.g. page endpoints that can redirect to an auth flow, should send the user to re-auth if `shouldRefreshCredentials` is `true`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const authResult = await panda.verify(headers.cookie);
|
|
85
|
+
if (authResult.success) {
|
|
86
|
+
if (authResult.shouldRefreshCredentials) {
|
|
87
|
+
// Send for auth
|
|
88
|
+
} else {
|
|
89
|
+
// Can perform action with user
|
|
90
|
+
return authResult.user;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Endpoints that cannot refresh credentials
|
|
96
|
+
|
|
97
|
+
Endpoints that **cannot** refresh credentials, e.g. API endpoints, should log appropriately and return something to the client that can be used to warn the user that they need to refresh their session.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const authResult = await panda.verify(headers.cookie);
|
|
101
|
+
if (authResult.success) {
|
|
102
|
+
const user = authResult.user;
|
|
103
|
+
// Handle request
|
|
104
|
+
// When returning response:
|
|
105
|
+
if (authResult.shouldRefreshCredentials) {
|
|
106
|
+
const mustRefreshByEpochTimeMillis =
|
|
107
|
+
authResult.mustRefreshByEpochTimeMillis;
|
|
108
|
+
const remainingTime = mustRefreshByEpochTimeMillis - Date.now();
|
|
109
|
+
console.warn(
|
|
110
|
+
`Stale Panda auth, will expire in ${remainingTime} milliseconds`
|
|
111
|
+
);
|
|
112
|
+
// Can still return 200, but depending on the type of API,
|
|
113
|
+
// we may want to return some extra information so the client
|
|
114
|
+
// can warn the user they need to refresh their session.
|
|
115
|
+
} else {
|
|
116
|
+
// It's a fresh session. Nothing to worry about!
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
3
121
|
## 0.5.1
|
|
4
122
|
|
|
5
123
|
### Patch Changes
|
package/CODEOWNERS
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
* @guardian/
|
|
1
|
+
* @guardian/workflow-and-collaboration
|
package/README.md
CHANGED
|
@@ -14,23 +14,53 @@ functionality for signing and verifying login cookies in Scala.
|
|
|
14
14
|
|
|
15
15
|
The `pan-domain-node` library provides an implementation of *verification only* for node apps.
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Grace period
|
|
18
|
+
We continue to consider the request authenticated for a period of time after the cookie expiry.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
This is to allow API requests which cannot directly send the user for re-auth to indicate to the user that they must take some action to refresh their credentials (usually, refreshing the page).
|
|
21
|
+
|
|
22
|
+
When the cookie is expired but we're still within this grace period, `shouldRefreshCredentials` will be `true`, which means:
|
|
23
|
+
- Endpoints that can refresh credentials (e.g. page endpoints that can redirect) should do so
|
|
24
|
+
- Endpoints that cannot refresh credentials (e.g. API endpoints) should tell the user to take some action to refresh credentials
|
|
20
25
|
|
|
26
|
+
```
|
|
27
|
+
Panda cookie: issued expires `mustRefreshByEpochTimeMillis`
|
|
28
|
+
| | |
|
|
29
|
+
|--1 hour--| |
|
|
30
|
+
Grace period: [------------- 24 hours ------]
|
|
31
|
+
|
|
32
|
+
`success`: --false-][-true-----------------------------------][-false-------->
|
|
33
|
+
`shouldRefreshCredentials` [-false---][-true------------------------]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Example usage
|
|
37
|
+
### Installation
|
|
38
|
+
[](https://badge.fury.io/js/%40guardian%2Fpan-domain-node)
|
|
21
39
|
```
|
|
22
40
|
npm install --save-dev @guardian/pan-domain-node
|
|
23
41
|
```
|
|
24
42
|
|
|
43
|
+
### Setup
|
|
44
|
+
The library load the public key file from a S3 object. Consuming applications can specify the S3 object via the arugments 'region', 'bucket' and 'keyFile' to the constructor of `PanDomainAuthentication` class as shown in the Initialisation below.
|
|
45
|
+
|
|
46
|
+
Therefore, the application must run with an AWS credential that has read access to the S3 object in that bucket.
|
|
47
|
+
|
|
48
|
+
You may refer to [Pan Domain authentication documentation](https://github.com/guardian/pan-domain-authentication) for details on how this authentication works.
|
|
49
|
+
|
|
50
|
+
### Initialisation
|
|
25
51
|
```typescript
|
|
26
52
|
import { PanDomainAuthentication, AuthenticationStatus, User, guardianValidation } from '@guardian/pan-domain-node';
|
|
53
|
+
import { fromIni } from "@aws-sdk/credential-providers";
|
|
54
|
+
|
|
55
|
+
const credentialsProvider = fromIni(); // get credentials locally using the default profile
|
|
27
56
|
|
|
28
57
|
const panda = new PanDomainAuthentication(
|
|
29
58
|
"gutoolsAuth-assym", // cookie name
|
|
30
59
|
"eu-west-1", // AWS region
|
|
31
60
|
"pan-domain-auth-settings", // Settings bucket
|
|
32
61
|
"local.dev-gutools.co.uk.settings.public", // Settings file
|
|
33
|
-
guardianValidation
|
|
62
|
+
guardianValidation,
|
|
63
|
+
credentialsProvider, // it can be omitted if the app runs in AWS cloud. In this case, "fromNodeProviderChain" is used by default.
|
|
34
64
|
);
|
|
35
65
|
|
|
36
66
|
// alternatively customise the validation function and pass at construction
|
|
@@ -38,18 +68,39 @@ function customValidation(user: User): boolean {
|
|
|
38
68
|
const isInCorrectDomain = user.email.indexOf('test.com') !== -1;
|
|
39
69
|
return isInCorrectDomain && user.multifactor;
|
|
40
70
|
}
|
|
71
|
+
```
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
### Verification: page endpoints
|
|
74
|
+
This is for endpoints that **can** refresh credentials, e.g. a page endpoint that can redirect to an auth flow:
|
|
75
|
+
```typescript
|
|
76
|
+
const authenticationResult = await panda.verify(headers.cookie);
|
|
77
|
+
if (authenticationResult.success) {
|
|
78
|
+
if (authenticationResult.shouldRefreshCredentials) {
|
|
79
|
+
// Send for auth
|
|
80
|
+
} else {
|
|
81
|
+
// Can perform action with user
|
|
82
|
+
return authenticationResult.user;
|
|
52
83
|
}
|
|
53
|
-
});
|
|
54
84
|
}
|
|
55
85
|
```
|
|
86
|
+
|
|
87
|
+
### Verification: API endpoints
|
|
88
|
+
This is for endpoints that **cannot** refresh credentials, e.g. API endpoints:
|
|
89
|
+
```typescript
|
|
90
|
+
const authenticationResult = await panda.verify(headers.cookie);
|
|
91
|
+
if (authenticationResult.success) {
|
|
92
|
+
const user = authenticationResult.user;
|
|
93
|
+
// Handle request
|
|
94
|
+
// When returning response:
|
|
95
|
+
if (authenticationResult.shouldRefreshCredentials) {
|
|
96
|
+
const mustRefreshByEpochTimeMillis = authenticationResult.mustRefreshByEpochTimeMillis;
|
|
97
|
+
const remainingTime = mustRefreshByEpochTimeMillis - Date.now();
|
|
98
|
+
console.warn(`Stale Panda auth, will expire in ${remainingTime} milliseconds`);
|
|
99
|
+
// Can still return 200, but depending on the type of API,
|
|
100
|
+
// we may want to return some extra information so the client
|
|
101
|
+
// can warn the user they need to refresh their session.
|
|
102
|
+
} else {
|
|
103
|
+
// It's a fresh session. Nothing to worry about!
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
package/dist/src/api.d.ts
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
export { PanDomainAuthentication } from './panda';
|
|
2
|
-
export declare
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
NOT_AUTHORISED = "Not Authorised",
|
|
6
|
-
AUTHORISED = "Authorised"
|
|
2
|
+
export declare const gracePeriodInMillis: number;
|
|
3
|
+
interface Result {
|
|
4
|
+
success: boolean;
|
|
7
5
|
}
|
|
6
|
+
interface Success extends Result {
|
|
7
|
+
success: true;
|
|
8
|
+
shouldRefreshCredentials: boolean;
|
|
9
|
+
user: User;
|
|
10
|
+
}
|
|
11
|
+
interface Failure extends Result {
|
|
12
|
+
success: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FreshSuccess extends Success {
|
|
16
|
+
shouldRefreshCredentials: false;
|
|
17
|
+
}
|
|
18
|
+
export interface StaleSuccess extends Success {
|
|
19
|
+
shouldRefreshCredentials: true;
|
|
20
|
+
mustRefreshByEpochTimeMillis: number;
|
|
21
|
+
}
|
|
22
|
+
export interface UserValidationFailure extends Failure {
|
|
23
|
+
reason: 'invalid-user';
|
|
24
|
+
user: User;
|
|
25
|
+
}
|
|
26
|
+
export interface CookieFailure extends Failure {
|
|
27
|
+
reason: 'no-cookie' | 'invalid-cookie' | 'expired-cookie';
|
|
28
|
+
}
|
|
29
|
+
export interface UnknownFailure extends Failure {
|
|
30
|
+
reason: 'unknown';
|
|
31
|
+
}
|
|
32
|
+
export type AuthenticationResult = FreshSuccess | StaleSuccess | CookieFailure | UserValidationFailure | UnknownFailure;
|
|
8
33
|
export interface User {
|
|
9
34
|
firstName: string;
|
|
10
35
|
lastName: string;
|
|
@@ -15,9 +40,5 @@ export interface User {
|
|
|
15
40
|
expires: number;
|
|
16
41
|
multifactor: boolean;
|
|
17
42
|
}
|
|
18
|
-
export
|
|
19
|
-
status: AuthenticationStatus;
|
|
20
|
-
user?: User;
|
|
21
|
-
}
|
|
22
|
-
export declare type ValidateUserFn = (user: User) => boolean;
|
|
43
|
+
export type ValidateUserFn = (user: User) => boolean;
|
|
23
44
|
export declare function guardianValidation(user: User): boolean;
|
package/dist/src/api.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.gracePeriodInMillis = exports.PanDomainAuthentication = void 0;
|
|
4
|
+
exports.guardianValidation = guardianValidation;
|
|
4
5
|
var panda_1 = require("./panda");
|
|
5
6
|
Object.defineProperty(exports, "PanDomainAuthentication", { enumerable: true, get: function () { return panda_1.PanDomainAuthentication; } });
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// We continue to consider the request authenticated for
|
|
8
|
+
// a period of time after the cookie expiry. This is to allow
|
|
9
|
+
// API requests which cannot directly send the user for re-auth to
|
|
10
|
+
// indicate to the user that they must take some action to refresh their
|
|
11
|
+
// credentials (usually, refreshing the page).
|
|
12
|
+
// Panda cookie: issued expires
|
|
13
|
+
// | |
|
|
14
|
+
// |--1 hour--|
|
|
15
|
+
// Grace period: [------------- 24 hours ------]
|
|
16
|
+
// `success`: --false-][-true-----------------------------------][-false-------->
|
|
17
|
+
// `shouldRefreshCredentials` [-false---][-true------------------------]
|
|
18
|
+
exports.gracePeriodInMillis = 24 * 60 * 60 * 1000;
|
|
13
19
|
function guardianValidation(user) {
|
|
14
20
|
const isGuardianUser = user.email.indexOf('guardian.co.uk') !== -1;
|
|
15
21
|
return isGuardianUser && user.multifactor;
|
|
16
22
|
}
|
|
17
|
-
exports.guardianValidation = guardianValidation;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { S3 } from "@aws-sdk/client-s3";
|
|
1
2
|
export interface PublicKeyHolder {
|
|
2
3
|
key: string;
|
|
3
4
|
lastUpdated: Date;
|
|
4
5
|
}
|
|
5
|
-
export declare function fetchPublicKey(
|
|
6
|
+
export declare function fetchPublicKey(s3: S3, bucket: string, keyFile: string): Promise<PublicKeyHolder>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -11,30 +15,54 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
11
15
|
}) : function(o, v) {
|
|
12
16
|
o["default"] = v;
|
|
13
17
|
});
|
|
14
|
-
var __importStar = (this && this.__importStar) || function (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
21
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.fetchPublicKey =
|
|
36
|
+
exports.fetchPublicKey = fetchPublicKey;
|
|
23
37
|
const iniparser = __importStar(require("iniparser"));
|
|
24
38
|
const utils_1 = require("./utils");
|
|
25
|
-
function fetchPublicKey(
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
function fetchPublicKey(s3, bucket, keyFile) {
|
|
40
|
+
const publicKeyLocation = {
|
|
41
|
+
Bucket: bucket,
|
|
42
|
+
Key: keyFile,
|
|
43
|
+
};
|
|
44
|
+
return s3.getObject(publicKeyLocation)
|
|
45
|
+
.then(({ Body }) => Body === null || Body === void 0 ? void 0 : Body.transformToString())
|
|
46
|
+
.then((pandaConfigIni) => {
|
|
47
|
+
if (!pandaConfigIni) {
|
|
48
|
+
throw Error(`could not read panda config ${JSON.stringify(publicKeyLocation)}`);
|
|
34
49
|
}
|
|
35
50
|
else {
|
|
36
|
-
|
|
51
|
+
const config = iniparser.parseString(pandaConfigIni);
|
|
52
|
+
if (config.publicKey) {
|
|
53
|
+
return {
|
|
54
|
+
key: (0, utils_1.base64ToPEM)(config.publicKey, "PUBLIC"),
|
|
55
|
+
lastUpdated: new Date()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(`Failed to retrieve panda public key from ${JSON.stringify(config)}`);
|
|
60
|
+
throw new Error("Missing publicKey setting from config");
|
|
61
|
+
}
|
|
37
62
|
}
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
console.error(`Error fetching public key from S3: ${error}`);
|
|
66
|
+
throw error;
|
|
38
67
|
});
|
|
39
68
|
}
|
|
40
|
-
exports.fetchPublicKey = fetchPublicKey;
|
package/dist/src/panda.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
1
|
import { User, AuthenticationResult, ValidateUserFn } from './api';
|
|
3
2
|
import { PublicKeyHolder } from './fetch-public-key';
|
|
3
|
+
import { S3 } from "@aws-sdk/client-s3";
|
|
4
|
+
import { AwsCredentialIdentityProvider } from "@aws-sdk/types";
|
|
4
5
|
export declare function createCookie(user: User, privateKey: string): string;
|
|
5
6
|
export declare function verifyUser(pandaCookie: string | undefined, publicKey: string, currentTime: Date, validateUser: ValidateUserFn): AuthenticationResult;
|
|
6
7
|
export declare class PanDomainAuthentication {
|
|
@@ -10,9 +11,10 @@ export declare class PanDomainAuthentication {
|
|
|
10
11
|
keyFile: string;
|
|
11
12
|
validateUser: ValidateUserFn;
|
|
12
13
|
publicKey: Promise<PublicKeyHolder>;
|
|
13
|
-
|
|
14
|
+
keyCacheTimeInMillis: number;
|
|
14
15
|
keyUpdateTimer?: NodeJS.Timeout;
|
|
15
|
-
|
|
16
|
+
s3Client: S3;
|
|
17
|
+
constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn, credentialsProvider?: AwsCredentialIdentityProvider);
|
|
16
18
|
stop(): void;
|
|
17
19
|
getPublicKey(): Promise<string>;
|
|
18
20
|
verify(requestCookies: string): Promise<AuthenticationResult>;
|
package/dist/src/panda.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -11,19 +15,33 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
11
15
|
}) : function(o, v) {
|
|
12
16
|
o["default"] = v;
|
|
13
17
|
});
|
|
14
|
-
var __importStar = (this && this.__importStar) || function (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
21
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.PanDomainAuthentication =
|
|
36
|
+
exports.PanDomainAuthentication = void 0;
|
|
37
|
+
exports.createCookie = createCookie;
|
|
38
|
+
exports.verifyUser = verifyUser;
|
|
23
39
|
const cookie = __importStar(require("cookie"));
|
|
24
40
|
const utils_1 = require("./utils");
|
|
25
41
|
const api_1 = require("./api");
|
|
26
42
|
const fetch_public_key_1 = require("./fetch-public-key");
|
|
43
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
44
|
+
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
27
45
|
function createCookie(user, privateKey) {
|
|
28
46
|
let queryParams = [];
|
|
29
47
|
queryParams.push("firstName=" + user.firstName);
|
|
@@ -36,46 +54,87 @@ function createCookie(user, privateKey) {
|
|
|
36
54
|
queryParams.push("multifactor=" + String(user.multifactor));
|
|
37
55
|
const combined = queryParams.join("&");
|
|
38
56
|
const queryParamsString = Buffer.from(combined).toString('base64');
|
|
39
|
-
const signature = utils_1.sign(combined, privateKey);
|
|
57
|
+
const signature = (0, utils_1.sign)(combined, privateKey);
|
|
40
58
|
return queryParamsString + "." + signature;
|
|
41
59
|
}
|
|
42
|
-
exports.createCookie = createCookie;
|
|
43
60
|
function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
|
|
44
61
|
if (!pandaCookie) {
|
|
45
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
reason: 'no-cookie'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const parsedCookie = (0, utils_1.parseCookie)(pandaCookie);
|
|
68
|
+
if (!parsedCookie) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
reason: 'invalid-cookie'
|
|
72
|
+
};
|
|
46
73
|
}
|
|
47
|
-
const { data, signature } =
|
|
48
|
-
if (!utils_1.verifySignature(data, signature, publicKey)) {
|
|
49
|
-
return {
|
|
74
|
+
const { data, signature } = parsedCookie;
|
|
75
|
+
if (!(0, utils_1.verifySignature)(data, signature, publicKey)) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
reason: 'invalid-cookie'
|
|
79
|
+
};
|
|
50
80
|
}
|
|
51
|
-
const
|
|
81
|
+
const currentTimestampInMillis = currentTime.getTime();
|
|
52
82
|
try {
|
|
53
|
-
const user = utils_1.parseUser(data);
|
|
54
|
-
const isExpired = user.expires <
|
|
83
|
+
const user = (0, utils_1.parseUser)(data);
|
|
84
|
+
const isExpired = user.expires < currentTimestampInMillis;
|
|
55
85
|
if (isExpired) {
|
|
56
|
-
|
|
86
|
+
const gracePeriodEndsAtEpochTimeMillis = user.expires + api_1.gracePeriodInMillis;
|
|
87
|
+
if (gracePeriodEndsAtEpochTimeMillis < currentTimestampInMillis) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
reason: 'expired-cookie'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
shouldRefreshCredentials: true,
|
|
97
|
+
mustRefreshByEpochTimeMillis: gracePeriodEndsAtEpochTimeMillis,
|
|
98
|
+
user
|
|
99
|
+
};
|
|
100
|
+
}
|
|
57
101
|
}
|
|
58
102
|
if (!validateUser(user)) {
|
|
59
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
reason: 'invalid-user',
|
|
106
|
+
user
|
|
107
|
+
};
|
|
60
108
|
}
|
|
61
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
shouldRefreshCredentials: false,
|
|
112
|
+
user
|
|
113
|
+
};
|
|
62
114
|
}
|
|
63
115
|
catch (error) {
|
|
64
116
|
console.error(error);
|
|
65
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
reason: 'unknown'
|
|
120
|
+
};
|
|
66
121
|
}
|
|
67
122
|
}
|
|
68
|
-
exports.verifyUser = verifyUser;
|
|
69
123
|
class PanDomainAuthentication {
|
|
70
|
-
constructor(cookieName, region, bucket, keyFile, validateUser) {
|
|
71
|
-
this.
|
|
124
|
+
constructor(cookieName, region, bucket, keyFile, validateUser, credentialsProvider = (0, credential_providers_1.fromNodeProviderChain)()) {
|
|
125
|
+
this.keyCacheTimeInMillis = 60 * 1000; // 1 minute
|
|
72
126
|
this.cookieName = cookieName;
|
|
73
127
|
this.region = region;
|
|
74
128
|
this.bucket = bucket;
|
|
75
129
|
this.keyFile = keyFile;
|
|
76
130
|
this.validateUser = validateUser;
|
|
77
|
-
|
|
78
|
-
|
|
131
|
+
const standardAwsConfig = {
|
|
132
|
+
region: region,
|
|
133
|
+
credentials: credentialsProvider,
|
|
134
|
+
};
|
|
135
|
+
this.s3Client = new client_s3_1.S3(standardAwsConfig);
|
|
136
|
+
this.publicKey = (0, fetch_public_key_1.fetchPublicKey)(this.s3Client, bucket, keyFile);
|
|
137
|
+
this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTimeInMillis);
|
|
79
138
|
}
|
|
80
139
|
stop() {
|
|
81
140
|
if (this.keyUpdateTimer) {
|
|
@@ -87,8 +146,8 @@ class PanDomainAuthentication {
|
|
|
87
146
|
return this.publicKey.then(({ key, lastUpdated }) => {
|
|
88
147
|
const now = new Date();
|
|
89
148
|
const diff = now.getTime() - lastUpdated.getTime();
|
|
90
|
-
if (diff > this.
|
|
91
|
-
this.publicKey = fetch_public_key_1.fetchPublicKey(this.
|
|
149
|
+
if (diff > this.keyCacheTimeInMillis) {
|
|
150
|
+
this.publicKey = (0, fetch_public_key_1.fetchPublicKey)(this.s3Client, this.bucket, this.keyFile);
|
|
92
151
|
return this.publicKey.then(({ key }) => key);
|
|
93
152
|
}
|
|
94
153
|
else {
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { User } from './api';
|
|
2
2
|
export declare function decodeBase64(data: string): string;
|
|
3
|
-
|
|
4
|
-
* Parse a pan-domain user cookie in to data and signature
|
|
5
|
-
*/
|
|
6
|
-
export declare function parseCookie(cookie: string): {
|
|
3
|
+
export type ParsedCookie = {
|
|
7
4
|
data: string;
|
|
8
5
|
signature: string;
|
|
9
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Parse a pan-domain user cookie in to data and signature
|
|
9
|
+
* Validates that the cookie is properly formatted (two base64 strings separated by '.')
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseCookie(cookie: string): ParsedCookie | undefined;
|
|
10
12
|
/**
|
|
11
13
|
* Verify signed data using nodeJs crypto library
|
|
12
14
|
*/
|