@stonyx/oauth 0.1.1-alpha.33 → 0.1.1-alpha.34
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 +1 -1
- package/dist/ticket-store.d.ts +21 -6
- package/dist/ticket-store.js +21 -6
- package/package.json +1 -1
- package/src/ticket-store.ts +21 -6
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ What the fragment does **not** remove is browser history and readability by page
|
|
|
257
257
|
| Entropy | 32 random bytes, base64url | Independent of the session id, never derived from it. |
|
|
258
258
|
| Authenticates | **nothing** | `GET /auth` validates against the session store, which has never heard of the ticket. A ticket in a `session-id` header is a `401`. |
|
|
259
259
|
| Failure modes | one indistinguishable `400` | Unknown, spent, expired and unparseable are not told apart. |
|
|
260
|
-
| Server-side storage | **
|
|
260
|
+
| Server-side storage | **keyed by the ticket digest** | The store is keyed by the SHA-256 of the ticket, never by the ticket, so the map holds no redeemable *ticket*: a reader of the map gets a digest, and a digest cannot be presented to the exchange. **It does still hold the live `sessionId` in plaintext, in the record value**, so the map is sensitive and must not be dumped or logged. Note this is the mirror image of the `oauth_state` binding rather than the same shape: `pendingStates` is keyed by the plaintext state and keeps the digest (`bindingHash`) in the value, so that record unlocks nothing on its own; here the digest is the key and the value is a live credential. Both share the discipline of never storing the client-presented secret in the clear. No constant-time compare is needed: lookup is a hash probe on a 256-bit key, not a secret-dependent byte comparison. |
|
|
261
261
|
| Exchange response | `Cache-Control: no-store` | The `200` body is the session id. A `POST` is not cacheable without explicit freshness, so this is defence in depth — no intermediary or service worker retains the credential. |
|
|
262
262
|
|
|
263
263
|
### Known residual risk
|
package/dist/ticket-store.d.ts
CHANGED
|
@@ -78,11 +78,26 @@ export default class TicketStore {
|
|
|
78
78
|
/**
|
|
79
79
|
* Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
81
|
+
* Keying by the digest means the map holds no redeemable *ticket*: a ticket
|
|
82
|
+
* is a client-presented secret looked up server-side, so what a reader of
|
|
83
|
+
* this map gets is a digest, and a digest cannot be presented to `redeem`.
|
|
84
|
+
*
|
|
85
|
+
* That does not make the map safe to expose. The record *value* holds a
|
|
86
|
+
* plaintext, live `sessionId` — the 24-hour bearer credential this store
|
|
87
|
+
* exists to keep out of URLs — so a heap dump, a debug serialisation or an
|
|
88
|
+
* accidental log of this map yields live session ids. The map is sensitive
|
|
89
|
+
* on that basis and must not be dumped or logged. Whether the stored
|
|
90
|
+
* `sessionId` should itself be protected is a separate question, and is not
|
|
91
|
+
* settled here.
|
|
92
|
+
*
|
|
93
|
+
* This is the mirror image of `OAuth.pendingStates`, not the same shape:
|
|
94
|
+
* there the *key* is the plaintext state token and the digest
|
|
95
|
+
* (`bindingHash`) sits in the value, so that record unlocks nothing on its
|
|
96
|
+
* own; here the digest is the key and the value is a live credential. What
|
|
97
|
+
* the two stores share is the discipline of never keeping a
|
|
98
|
+
* client-presented secret in the clear — neither the ticket nor the binding
|
|
99
|
+
* value is on the heap — but they place the digest on opposite sides of the
|
|
100
|
+
* entry.
|
|
86
101
|
*
|
|
87
102
|
* No constant-time comparison is needed and none is used: lookup is a hash
|
|
88
103
|
* probe on a 256-bit high-entropy key, not a secret-dependent byte
|
|
@@ -91,7 +106,7 @@ export default class TicketStore {
|
|
|
91
106
|
*/
|
|
92
107
|
tickets: Map<string, TicketRecord>;
|
|
93
108
|
ttl: number;
|
|
94
|
-
/** SHA-256 of a ticket, hex — the only form this store keeps
|
|
109
|
+
/** SHA-256 of a ticket, hex — the only form of the *ticket* this store keeps. */
|
|
95
110
|
static hash(ticket: string): string;
|
|
96
111
|
/**
|
|
97
112
|
* Mints a ticket for a freshly created session.
|
package/dist/ticket-store.js
CHANGED
|
@@ -70,11 +70,26 @@ export default class TicketStore {
|
|
|
70
70
|
/**
|
|
71
71
|
* Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
|
|
72
72
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
73
|
+
* Keying by the digest means the map holds no redeemable *ticket*: a ticket
|
|
74
|
+
* is a client-presented secret looked up server-side, so what a reader of
|
|
75
|
+
* this map gets is a digest, and a digest cannot be presented to `redeem`.
|
|
76
|
+
*
|
|
77
|
+
* That does not make the map safe to expose. The record *value* holds a
|
|
78
|
+
* plaintext, live `sessionId` — the 24-hour bearer credential this store
|
|
79
|
+
* exists to keep out of URLs — so a heap dump, a debug serialisation or an
|
|
80
|
+
* accidental log of this map yields live session ids. The map is sensitive
|
|
81
|
+
* on that basis and must not be dumped or logged. Whether the stored
|
|
82
|
+
* `sessionId` should itself be protected is a separate question, and is not
|
|
83
|
+
* settled here.
|
|
84
|
+
*
|
|
85
|
+
* This is the mirror image of `OAuth.pendingStates`, not the same shape:
|
|
86
|
+
* there the *key* is the plaintext state token and the digest
|
|
87
|
+
* (`bindingHash`) sits in the value, so that record unlocks nothing on its
|
|
88
|
+
* own; here the digest is the key and the value is a live credential. What
|
|
89
|
+
* the two stores share is the discipline of never keeping a
|
|
90
|
+
* client-presented secret in the clear — neither the ticket nor the binding
|
|
91
|
+
* value is on the heap — but they place the digest on opposite sides of the
|
|
92
|
+
* entry.
|
|
78
93
|
*
|
|
79
94
|
* No constant-time comparison is needed and none is used: lookup is a hash
|
|
80
95
|
* probe on a 256-bit high-entropy key, not a secret-dependent byte
|
|
@@ -83,7 +98,7 @@ export default class TicketStore {
|
|
|
83
98
|
*/
|
|
84
99
|
tickets = new Map();
|
|
85
100
|
ttl = TICKET_TTL_MS;
|
|
86
|
-
/** SHA-256 of a ticket, hex — the only form this store keeps
|
|
101
|
+
/** SHA-256 of a ticket, hex — the only form of the *ticket* this store keeps. */
|
|
87
102
|
static hash(ticket) {
|
|
88
103
|
return createHash('sha256').update(ticket).digest('hex');
|
|
89
104
|
}
|
package/package.json
CHANGED
package/src/ticket-store.ts
CHANGED
|
@@ -84,11 +84,26 @@ export default class TicketStore {
|
|
|
84
84
|
/**
|
|
85
85
|
* Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
|
|
86
86
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
87
|
+
* Keying by the digest means the map holds no redeemable *ticket*: a ticket
|
|
88
|
+
* is a client-presented secret looked up server-side, so what a reader of
|
|
89
|
+
* this map gets is a digest, and a digest cannot be presented to `redeem`.
|
|
90
|
+
*
|
|
91
|
+
* That does not make the map safe to expose. The record *value* holds a
|
|
92
|
+
* plaintext, live `sessionId` — the 24-hour bearer credential this store
|
|
93
|
+
* exists to keep out of URLs — so a heap dump, a debug serialisation or an
|
|
94
|
+
* accidental log of this map yields live session ids. The map is sensitive
|
|
95
|
+
* on that basis and must not be dumped or logged. Whether the stored
|
|
96
|
+
* `sessionId` should itself be protected is a separate question, and is not
|
|
97
|
+
* settled here.
|
|
98
|
+
*
|
|
99
|
+
* This is the mirror image of `OAuth.pendingStates`, not the same shape:
|
|
100
|
+
* there the *key* is the plaintext state token and the digest
|
|
101
|
+
* (`bindingHash`) sits in the value, so that record unlocks nothing on its
|
|
102
|
+
* own; here the digest is the key and the value is a live credential. What
|
|
103
|
+
* the two stores share is the discipline of never keeping a
|
|
104
|
+
* client-presented secret in the clear — neither the ticket nor the binding
|
|
105
|
+
* value is on the heap — but they place the digest on opposite sides of the
|
|
106
|
+
* entry.
|
|
92
107
|
*
|
|
93
108
|
* No constant-time comparison is needed and none is used: lookup is a hash
|
|
94
109
|
* probe on a 256-bit high-entropy key, not a secret-dependent byte
|
|
@@ -98,7 +113,7 @@ export default class TicketStore {
|
|
|
98
113
|
tickets = new Map<string, TicketRecord>();
|
|
99
114
|
ttl = TICKET_TTL_MS;
|
|
100
115
|
|
|
101
|
-
/** SHA-256 of a ticket, hex — the only form this store keeps
|
|
116
|
+
/** SHA-256 of a ticket, hex — the only form of the *ticket* this store keeps. */
|
|
102
117
|
static hash(ticket: string): string {
|
|
103
118
|
return createHash('sha256').update(ticket).digest('hex');
|
|
104
119
|
}
|