@stonyx/oauth 0.1.1-alpha.30 → 0.1.1-alpha.32

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.
@@ -75,8 +75,24 @@ export interface RedeemedTicket {
75
75
  * The extraction is tracked at `abofs/stonyx-oauth#58`.
76
76
  */
77
77
  export default class TicketStore {
78
+ /**
79
+ * Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
80
+ *
81
+ * Same discipline as `OAuth.pendingStates`, which holds `bindingHash` and
82
+ * never the binding value: a ticket is a client-presented secret looked up
83
+ * server-side, so a heap dump, a debug serialisation or an accidental log of
84
+ * this map should yield a useless digest rather than a live redeemable
85
+ * credential. The two secret stores in this module now agree.
86
+ *
87
+ * No constant-time comparison is needed and none is used: lookup is a hash
88
+ * probe on a 256-bit high-entropy key, not a secret-dependent byte
89
+ * comparison, so there is no early-exit timing signal to exploit. That is
90
+ * the same reason `redeem` can stay an ordinary `Map.get`.
91
+ */
78
92
  tickets: Map<string, TicketRecord>;
79
93
  ttl: number;
94
+ /** SHA-256 of a ticket, hex — the only form this store keeps on the heap. */
95
+ static hash(ticket: string): string;
80
96
  /**
81
97
  * Mints a ticket for a freshly created session.
82
98
  *
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  /**
3
3
  * Lifetime of an exchange ticket.
4
4
  *
@@ -67,8 +67,26 @@ export const TICKET_BYTES = 32;
67
67
  * The extraction is tracked at `abofs/stonyx-oauth#58`.
68
68
  */
69
69
  export default class TicketStore {
70
+ /**
71
+ * Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
72
+ *
73
+ * Same discipline as `OAuth.pendingStates`, which holds `bindingHash` and
74
+ * never the binding value: a ticket is a client-presented secret looked up
75
+ * server-side, so a heap dump, a debug serialisation or an accidental log of
76
+ * this map should yield a useless digest rather than a live redeemable
77
+ * credential. The two secret stores in this module now agree.
78
+ *
79
+ * No constant-time comparison is needed and none is used: lookup is a hash
80
+ * probe on a 256-bit high-entropy key, not a secret-dependent byte
81
+ * comparison, so there is no early-exit timing signal to exploit. That is
82
+ * the same reason `redeem` can stay an ordinary `Map.get`.
83
+ */
70
84
  tickets = new Map();
71
85
  ttl = TICKET_TTL_MS;
86
+ /** SHA-256 of a ticket, hex — the only form this store keeps on the heap. */
87
+ static hash(ticket) {
88
+ return createHash('sha256').update(ticket).digest('hex');
89
+ }
72
90
  /**
73
91
  * Mints a ticket for a freshly created session.
74
92
  *
@@ -77,7 +95,7 @@ export default class TicketStore {
77
95
  */
78
96
  issue(sessionId, expiresAt) {
79
97
  const ticket = randomBytes(TICKET_BYTES).toString('base64url');
80
- this.tickets.set(ticket, { sessionId, expiresAt, createdAt: Date.now() });
98
+ this.tickets.set(TicketStore.hash(ticket), { sessionId, expiresAt, createdAt: Date.now() });
81
99
  return ticket;
82
100
  }
83
101
  /**
@@ -95,10 +113,11 @@ export default class TicketStore {
95
113
  * holder of a ticket they did not mint has no business having.
96
114
  */
97
115
  redeem(ticket) {
98
- const record = ticket ? this.tickets.get(ticket) : undefined;
116
+ const key = ticket ? TicketStore.hash(ticket) : null;
117
+ const record = key ? this.tickets.get(key) : undefined;
99
118
  if (!record)
100
119
  return null;
101
- this.tickets.delete(ticket);
120
+ this.tickets.delete(key);
102
121
  if (Date.now() - record.createdAt > this.ttl)
103
122
  return null;
104
123
  return { sessionId: record.sessionId, expiresAt: record.expiresAt };
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.1.1-alpha.30",
7
+ "version": "0.1.1-alpha.32",
8
8
  "description": "OAuth2 authentication module for the Stonyx framework",
9
9
  "repository": {
10
10
  "type": "git",
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
 
3
3
  /**
4
4
  * Lifetime of an exchange ticket.
@@ -81,9 +81,28 @@ export interface RedeemedTicket {
81
81
  * The extraction is tracked at `abofs/stonyx-oauth#58`.
82
82
  */
83
83
  export default class TicketStore {
84
+ /**
85
+ * Live tickets, keyed by the **SHA-256 of the ticket**, never by the ticket.
86
+ *
87
+ * Same discipline as `OAuth.pendingStates`, which holds `bindingHash` and
88
+ * never the binding value: a ticket is a client-presented secret looked up
89
+ * server-side, so a heap dump, a debug serialisation or an accidental log of
90
+ * this map should yield a useless digest rather than a live redeemable
91
+ * credential. The two secret stores in this module now agree.
92
+ *
93
+ * No constant-time comparison is needed and none is used: lookup is a hash
94
+ * probe on a 256-bit high-entropy key, not a secret-dependent byte
95
+ * comparison, so there is no early-exit timing signal to exploit. That is
96
+ * the same reason `redeem` can stay an ordinary `Map.get`.
97
+ */
84
98
  tickets = new Map<string, TicketRecord>();
85
99
  ttl = TICKET_TTL_MS;
86
100
 
101
+ /** SHA-256 of a ticket, hex — the only form this store keeps on the heap. */
102
+ static hash(ticket: string): string {
103
+ return createHash('sha256').update(ticket).digest('hex');
104
+ }
105
+
87
106
  /**
88
107
  * Mints a ticket for a freshly created session.
89
108
  *
@@ -92,7 +111,7 @@ export default class TicketStore {
92
111
  */
93
112
  issue(sessionId: string, expiresAt: number): string {
94
113
  const ticket = randomBytes(TICKET_BYTES).toString('base64url');
95
- this.tickets.set(ticket, { sessionId, expiresAt, createdAt: Date.now() });
114
+ this.tickets.set(TicketStore.hash(ticket), { sessionId, expiresAt, createdAt: Date.now() });
96
115
  return ticket;
97
116
  }
98
117
 
@@ -111,10 +130,11 @@ export default class TicketStore {
111
130
  * holder of a ticket they did not mint has no business having.
112
131
  */
113
132
  redeem(ticket: string): RedeemedTicket | null {
114
- const record = ticket ? this.tickets.get(ticket) : undefined;
133
+ const key = ticket ? TicketStore.hash(ticket) : null;
134
+ const record = key ? this.tickets.get(key) : undefined;
115
135
  if (!record) return null;
116
136
 
117
- this.tickets.delete(ticket);
137
+ this.tickets.delete(key!);
118
138
 
119
139
  if (Date.now() - record.createdAt > this.ttl) return null;
120
140