@stonyx/oauth 0.1.1-alpha.2 → 0.1.1-alpha.21

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.
Files changed (44) hide show
  1. package/README.md +170 -3
  2. package/dist/auth-request.d.ts +147 -0
  3. package/dist/auth-request.js +342 -0
  4. package/dist/constants.d.ts +33 -0
  5. package/dist/constants.js +42 -0
  6. package/dist/main.d.ts +45 -0
  7. package/dist/main.js +81 -0
  8. package/dist/oauth-flow.d.ts +30 -0
  9. package/dist/oauth-flow.js +83 -0
  10. package/dist/providers/discord.d.ts +30 -0
  11. package/dist/providers/discord.js +43 -0
  12. package/dist/session-manager.d.ts +20 -0
  13. package/dist/session-manager.js +30 -0
  14. package/dist/state-store.d.ts +116 -0
  15. package/dist/state-store.js +144 -0
  16. package/dist/token-manager.d.ts +15 -0
  17. package/dist/token-manager.js +24 -0
  18. package/package.json +45 -9
  19. package/src/auth-request.ts +434 -0
  20. package/src/constants.ts +46 -0
  21. package/src/main.ts +123 -0
  22. package/src/{oauth-flow.js → oauth-flow.ts} +31 -7
  23. package/src/providers/{discord.js → discord.ts} +29 -3
  24. package/src/{session-manager.js → session-manager.ts} +19 -6
  25. package/src/state-store.ts +179 -0
  26. package/src/token-manager.ts +35 -0
  27. package/src/types/node.d.ts +10 -0
  28. package/src/types/stonyx-events.d.ts +4 -0
  29. package/src/types/stonyx-rest-server.d.ts +11 -0
  30. package/src/types/stonyx.d.ts +38 -0
  31. package/.github/workflows/ci.yml +0 -16
  32. package/.github/workflows/publish.yml +0 -51
  33. package/src/auth-request.js +0 -74
  34. package/src/main.js +0 -83
  35. package/src/token-manager.js +0 -26
  36. package/test/config/environment.js +0 -18
  37. package/test/integration/oauth-test.js +0 -149
  38. package/test/sample/providers/mock.js +0 -40
  39. package/test/sample/requests/.gitkeep +0 -0
  40. package/test/unit/oauth-flow-test.js +0 -137
  41. package/test/unit/providers/discord-test.js +0 -115
  42. package/test/unit/session-manager-test.js +0 -85
  43. package/test/unit/state-validation-test.js +0 -118
  44. package/test/unit/token-manager-test.js +0 -76
@@ -1,7 +1,33 @@
1
1
  import OAuthFlow from '../oauth-flow.js';
2
+ import type { TokenResult } from '../oauth-flow.js';
3
+
4
+ interface DiscordProviderConfig {
5
+ clientId: string;
6
+ clientSecret: string;
7
+ redirectUri: string;
8
+ scopes?: string[];
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ interface DiscordUser {
13
+ id: string;
14
+ username: string;
15
+ global_name?: string;
16
+ avatar: string | null;
17
+ email?: string | null;
18
+ }
19
+
20
+ interface NormalizedDiscordUser {
21
+ id: string;
22
+ username: string;
23
+ displayName: string;
24
+ avatar: string | null;
25
+ email: string | null;
26
+ raw: DiscordUser;
27
+ }
2
28
 
3
29
  export default class DiscordProvider extends OAuthFlow {
4
- constructor(config) {
30
+ constructor(config: DiscordProviderConfig) {
5
31
  super({
6
32
  ...config,
7
33
  authorizationUrl: 'https://discord.com/oauth2/authorize',
@@ -10,7 +36,7 @@ export default class DiscordProvider extends OAuthFlow {
10
36
  });
11
37
  }
12
38
 
13
- async exchangeCode(code) {
39
+ async exchangeCode(code: string): Promise<TokenResult> {
14
40
  const response = await fetch(this.tokenUrl, {
15
41
  method: 'POST',
16
42
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -34,7 +60,7 @@ export default class DiscordProvider extends OAuthFlow {
34
60
  };
35
61
  }
36
62
 
37
- normalizeUser(rawUser) {
63
+ override normalizeUser(rawUser: DiscordUser): NormalizedDiscordUser {
38
64
  const { id, username, global_name, avatar, email } = rawUser;
39
65
 
40
66
  return {
@@ -1,13 +1,26 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
+ interface SessionData {
4
+ user: unknown;
5
+ tokens: unknown;
6
+ expiresAt: number;
7
+ }
8
+
9
+ export interface SessionResult {
10
+ sessionId: string;
11
+ user: unknown;
12
+ expiresAt: number;
13
+ }
14
+
3
15
  export default class SessionManager {
4
- sessions = new Map();
16
+ sessions = new Map<string, SessionData>();
17
+ duration: number;
5
18
 
6
- constructor(duration) {
19
+ constructor(duration: number) {
7
20
  this.duration = duration;
8
21
  }
9
22
 
10
- create(user, tokens) {
23
+ create(user: unknown, tokens: unknown): SessionResult {
11
24
  const sessionId = randomUUID();
12
25
  const expiresAt = Date.now() + (this.duration * 1000);
13
26
 
@@ -16,15 +29,15 @@ export default class SessionManager {
16
29
  return { sessionId, user, expiresAt };
17
30
  }
18
31
 
19
- get(sessionId) {
32
+ get(sessionId: string): SessionData | null {
20
33
  return this.sessions.get(sessionId) || null;
21
34
  }
22
35
 
23
- destroy(sessionId) {
36
+ destroy(sessionId: string): void {
24
37
  this.sessions.delete(sessionId);
25
38
  }
26
39
 
27
- validate(sessionId) {
40
+ validate(sessionId: string): unknown {
28
41
  const session = this.get(sessionId);
29
42
  if (!session) return null;
30
43
 
@@ -0,0 +1,179 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+ import { BINDING_VALUE_BYTES, STATE_TTL_MS } from './constants.js';
3
+
4
+ /**
5
+ * Server-side record for an OAuth flow that is in progress.
6
+ *
7
+ * Deliberately holds a *digest* of the binding value rather than the value
8
+ * itself: a callback is only accepted when the caller presents the plaintext
9
+ * that hashes to `bindingHash`, so the record on its own unlocks nothing.
10
+ */
11
+ export interface PendingState {
12
+ provider: string;
13
+ bindingHash: string;
14
+ createdAt: number;
15
+ }
16
+
17
+ /**
18
+ * The five reasons a callback is rejected, as fixed strings.
19
+ *
20
+ * Named rather than inlined so that collapsing two of them into one is a
21
+ * visible edit: distinguishing them in the server log is the whole point of
22
+ * logging a reason, and an operator telling an expired state from a
23
+ * cross-provider replay depends on them staying distinct.
24
+ */
25
+ export const STATE_REJECTION = {
26
+ unknownState: 'Invalid or missing state token',
27
+ expired: 'State token has expired',
28
+ wrongProvider: 'State token was not issued for this provider',
29
+ missingBinding: 'Missing state binding value',
30
+ unboundClient: 'State token is not bound to this client',
31
+ } as const;
32
+
33
+ /**
34
+ * A callback rejected by `StateStore.consume`.
35
+ *
36
+ * Carries two things the route layer cannot otherwise recover: that the
37
+ * rejection came from state validation rather than from anything downstream of
38
+ * it, and whether a pending record was actually consumed.
39
+ */
40
+ export class StateRejection extends Error {
41
+ /** True when this attempt recognised a pending record and burned it. */
42
+ consumed: boolean;
43
+
44
+ constructor(reason: string, consumed: boolean) {
45
+ super(reason);
46
+ this.name = 'StateRejection';
47
+ this.consumed = consumed;
48
+ }
49
+ }
50
+
51
+ export interface IssuedState {
52
+ /** Sent to the provider as the OAuth2 `state` parameter. */
53
+ stateToken: string;
54
+ /** Held by the client that started the flow (a cookie), never by the provider. */
55
+ bindingValue: string;
56
+ }
57
+
58
+ /**
59
+ * Issues and validates OAuth2 `state` tokens bound to the client that started
60
+ * the flow (#36).
61
+ *
62
+ * Presence-plus-age on a process-global map is replay-window limiting, not the
63
+ * CSRF binding `state` exists to provide (RFC 6749 section 10.12): any state
64
+ * issued to any visitor validated for any callback, so an attacker could
65
+ * harvest their own state and code and deliver them to a victim, logging the
66
+ * victim in as the attacker. A state is now only accepted when the caller also
67
+ * presents the matching client-held binding value, and only at the provider it
68
+ * was issued for.
69
+ */
70
+ export default class StateStore {
71
+ pending = new Map<string, PendingState>();
72
+ ttl: number;
73
+
74
+ constructor(ttl: number = STATE_TTL_MS) {
75
+ this.ttl = ttl;
76
+ }
77
+
78
+ static hash(value: string): string {
79
+ return createHash('sha256').update(value).digest('hex');
80
+ }
81
+
82
+ /** Length-independent, content-constant-time comparison of two digests. */
83
+ static digestsMatch(a: string, b: string): boolean {
84
+ if (a.length !== b.length) return false;
85
+
86
+ let difference = 0;
87
+ for (let index = 0; index < a.length; index++) {
88
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
89
+ }
90
+
91
+ return difference === 0;
92
+ }
93
+
94
+ issue(provider: string): IssuedState {
95
+ const stateToken = randomUUID();
96
+ const bindingValue = randomBytes(BINDING_VALUE_BYTES).toString('base64url');
97
+
98
+ this.pending.set(stateToken, {
99
+ provider,
100
+ bindingHash: StateStore.hash(bindingValue),
101
+ createdAt: Date.now(),
102
+ });
103
+
104
+ return { stateToken, bindingValue };
105
+ }
106
+
107
+ /**
108
+ * Validates and consumes a pending state. Throws on every rejection path.
109
+ *
110
+ * The record is removed as soon as the state is recognised — before the TTL,
111
+ * provider and binding checks — so every state gets exactly one attempt
112
+ * whatever the outcome.
113
+ *
114
+ * That uniformity is the justification, not brute-force resistance:
115
+ * guessing `BINDING_VALUE_BYTES` of CSPRNG output is infeasible whether or
116
+ * not the record survives. What retaining it would buy an attacker is a
117
+ * repeatable, unauthenticated oracle on this endpoint for the state's full
118
+ * lifetime — and the safety of that would then rest entirely on an entropy
119
+ * constant a future change can lower. One attempt per state is a structural
120
+ * property; entropy arithmetic is not.
121
+ *
122
+ * The trade is real: an attacker who already knows a victim's state can burn
123
+ * it, and the victim must restart at `/auth/login/:provider`. That vector is
124
+ * accepted deliberately — it requires the victim's `randomUUID` state, and
125
+ * it is self-healing on retry. `consumed` on the rejection says whether this
126
+ * call actually burned a record, so a caller can distinguish "nothing of the
127
+ * victim's was touched" from "one attempt was spent".
128
+ *
129
+ * `bindingValues` is every value the client presented under the binding
130
+ * cookie's name, not just the first — see `anyCandidateMatches`.
131
+ */
132
+ consume(stateToken: string | undefined, provider: string, bindingValues: readonly string[]): void {
133
+ if (!stateToken) throw new StateRejection(STATE_REJECTION.unknownState, false);
134
+
135
+ const record = this.pending.get(stateToken);
136
+ if (!record) throw new StateRejection(STATE_REJECTION.unknownState, false);
137
+ this.pending.delete(stateToken);
138
+
139
+ if (Date.now() - record.createdAt > this.ttl) throw new StateRejection(STATE_REJECTION.expired, true);
140
+ if (record.provider !== provider) throw new StateRejection(STATE_REJECTION.wrongProvider, true);
141
+
142
+ const candidates = bindingValues.filter(value => value.length > 0);
143
+ if (candidates.length === 0) throw new StateRejection(STATE_REJECTION.missingBinding, true);
144
+
145
+ if (!this.anyCandidateMatches(candidates, record)) {
146
+ throw new StateRejection(STATE_REJECTION.unboundClient, true);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Whether *any* presented value is the binding value for this record.
152
+ *
153
+ * Every candidate is tried, and the callback is accepted if one matches.
154
+ * Returning on the first value carrying the cookie name instead made a
155
+ * planted cookie a permanent, unauthenticated denial of login: RFC 6265
156
+ * section 5.4 orders the `Cookie` header by path length then creation time,
157
+ * so an attacker with content control on a sibling subdomain sets a
158
+ * same-named cookie once and every subsequent callback for that victim reads
159
+ * theirs, fails the binding check, and burns the state on the way out. The
160
+ * victim cannot recover by retrying.
161
+ *
162
+ * Accepting any match gives an attacker nothing: they would have to present
163
+ * the victim's own binding value, which is the property being checked. The
164
+ * record is consumed on recognition, so a state still gets exactly one
165
+ * attempt however many candidates were presented, and the candidate count is
166
+ * bounded by Node's header size limit rather than by a cap here — a cap
167
+ * truncates the list from the wrong end and reinstates the denial this method
168
+ * exists to close. See `constants.ts`.
169
+ *
170
+ * The loop does not short-circuit, so the work is a function of how many
171
+ * values were presented and not of which one matched.
172
+ */
173
+ anyCandidateMatches(candidates: readonly string[], record: PendingState): boolean {
174
+ return candidates.reduce(
175
+ (matched, candidate) => StateStore.digestsMatch(StateStore.hash(candidate), record.bindingHash) || matched,
176
+ false,
177
+ );
178
+ }
179
+ }
@@ -0,0 +1,35 @@
1
+ import type OAuthFlow from './oauth-flow.js';
2
+ import type { TokenResult } from './oauth-flow.js';
3
+
4
+ export interface TokenData extends TokenResult {
5
+ expiresAt: number;
6
+ }
7
+
8
+ export default class TokenManager {
9
+ flow: OAuthFlow;
10
+
11
+ constructor(flow: OAuthFlow) {
12
+ this.flow = flow;
13
+ }
14
+
15
+ async getTokens(code: string): Promise<TokenData> {
16
+ const tokens = await this.flow.exchangeCode(code) as TokenData;
17
+ tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
18
+ return tokens;
19
+ }
20
+
21
+ async refresh(refreshToken: string): Promise<TokenData> {
22
+ const tokens = await this.flow.refreshAccessToken(refreshToken) as TokenData;
23
+ tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
24
+ return tokens;
25
+ }
26
+
27
+ async revoke(accessToken: string): Promise<void> {
28
+ return this.flow.revokeToken(accessToken);
29
+ }
30
+
31
+ isExpired(tokenData: { expiresAt?: number } | null | undefined): boolean {
32
+ if (!tokenData?.expiresAt) return true;
33
+ return Date.now() >= tokenData.expiresAt;
34
+ }
35
+ }
@@ -0,0 +1,10 @@
1
+ declare module 'node:crypto' {
2
+ interface Hash {
3
+ update(data: string): Hash;
4
+ digest(encoding: 'hex'): string;
5
+ }
6
+
7
+ export function randomUUID(): string;
8
+ export function randomBytes(size: number): { toString(encoding: 'base64url' | 'hex'): string };
9
+ export function createHash(algorithm: string): Hash;
10
+ }
@@ -0,0 +1,4 @@
1
+ declare module '@stonyx/events' {
2
+ export function setup(events: string[]): void;
3
+ export function emit(event: string, ...args: unknown[]): Promise<void>;
4
+ }
@@ -0,0 +1,11 @@
1
+ declare module '@stonyx/rest-server' {
2
+ export class Request {
3
+ constructor();
4
+ }
5
+
6
+ export default class RestServer {
7
+ static instance: RestServer;
8
+ static close(): void;
9
+ mountRoute(RequestClass: unknown, options: { name: string; options?: unknown }): void;
10
+ }
11
+ }
@@ -0,0 +1,38 @@
1
+ declare module 'stonyx/config' {
2
+ interface OAuthConfig {
3
+ providers: Record<string, { module?: string; [key: string]: unknown }>;
4
+ sessionDuration: number;
5
+ frontendCallbackUrl?: string;
6
+ logColor?: string;
7
+ logMethod?: string;
8
+ }
9
+ interface Config {
10
+ oauth: OAuthConfig;
11
+ rootPath: string;
12
+ [key: string]: unknown;
13
+ }
14
+ const config: Config;
15
+ export default config;
16
+ }
17
+
18
+ declare module 'stonyx/log' {
19
+ interface Log {
20
+ oauth(message: string): void;
21
+ error(message: string): void;
22
+ defineType(type: string, setting: string, options?: Record<string, unknown> | null): void;
23
+ [key: string]: unknown;
24
+ }
25
+ const log: Log;
26
+ export default log;
27
+ }
28
+
29
+ declare module 'stonyx' {
30
+ export function waitForModule(name: string): Promise<void>;
31
+ }
32
+
33
+ declare module 'stonyx/test-helpers' {
34
+ export function setupIntegrationTests(hooks: {
35
+ before(fn: () => void | Promise<void>): void;
36
+ after(fn: () => void | Promise<void>): void;
37
+ }): void;
38
+ }
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [dev, main]
6
-
7
- concurrency:
8
- group: ci-${{ github.head_ref || github.ref }}
9
- cancel-in-progress: true
10
-
11
- permissions:
12
- contents: read
13
-
14
- jobs:
15
- test:
16
- uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
@@ -1,51 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- repository_dispatch:
5
- types: [cascade-publish]
6
- workflow_dispatch:
7
- inputs:
8
- version-type:
9
- description: 'Version type'
10
- required: true
11
- type: choice
12
- options:
13
- - patch
14
- - minor
15
- - major
16
- custom-version:
17
- description: 'Custom version (optional, overrides version-type)'
18
- required: false
19
- type: string
20
- pull_request:
21
- types: [opened, synchronize, reopened]
22
- branches: [main]
23
- push:
24
- branches: [main]
25
-
26
- concurrency:
27
- group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
28
- cancel-in-progress: false
29
-
30
- permissions:
31
- contents: write
32
- id-token: write
33
- pull-requests: write
34
-
35
- jobs:
36
- publish:
37
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
38
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
39
- with:
40
- version-type: ${{ github.event.inputs.version-type }}
41
- custom-version: ${{ github.event.inputs.custom-version }}
42
- cascade-source: ${{ github.event.client_payload.source_package || '' }}
43
- secrets: inherit
44
-
45
- cascade:
46
- needs: publish
47
- uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
48
- with:
49
- package-name: ${{ needs.publish.outputs.package-name }}
50
- published-version: ${{ needs.publish.outputs.published-version }}
51
- secrets: inherit
@@ -1,74 +0,0 @@
1
- import { Request } from '@stonyx/rest-server';
2
-
3
- export default class AuthRequest extends Request {
4
- constructor(oauth) {
5
- super();
6
- this.oauth = oauth;
7
- }
8
-
9
- handlers = {
10
- get: {
11
- '/': ({ headers }) => {
12
- const sessionId = headers['session-id'];
13
- if (!sessionId) return 401;
14
-
15
- const user = this.oauth.getSession(sessionId);
16
- if (!user) return 401;
17
-
18
- return user;
19
- },
20
-
21
- '/login/:provider': (req, state) => {
22
- const { provider: providerName } = req.params;
23
-
24
- try {
25
- const url = this.oauth.getAuthorizationUrl(providerName);
26
- state.redirect = url;
27
- } catch {
28
- return 404;
29
- }
30
- },
31
-
32
- '/callback/:provider': async (req, state) => {
33
- const { provider: providerName } = req.params;
34
- const { code, state: stateToken, error } = req.query;
35
-
36
- if (error) {
37
- if (this.oauth.frontendCallbackUrl) {
38
- state.redirect = `${this.oauth.frontendCallbackUrl}?error=${encodeURIComponent(error)}`;
39
- return;
40
- }
41
- return 400;
42
- }
43
-
44
- if (!code) return 400;
45
-
46
- try {
47
- const session = await this.oauth.handleCallback(providerName, code, stateToken);
48
-
49
- if (this.oauth.frontendCallbackUrl) {
50
- const params = new URLSearchParams({
51
- sessionId: session.sessionId,
52
- expiresAt: session.expiresAt,
53
- });
54
- state.redirect = `${this.oauth.frontendCallbackUrl}?${params}`;
55
- return;
56
- }
57
-
58
- return session;
59
- } catch {
60
- if (this.oauth.frontendCallbackUrl) {
61
- state.redirect = `${this.oauth.frontendCallbackUrl}?error=auth_failed`;
62
- return;
63
- }
64
- return 500;
65
- }
66
- },
67
-
68
- '/logout': ({ headers }) => {
69
- const sessionId = headers['session-id'];
70
- if (sessionId) this.oauth.logout(sessionId);
71
- },
72
- }
73
- };
74
- }
package/src/main.js DELETED
@@ -1,83 +0,0 @@
1
- import config from 'stonyx/config';
2
- import log from 'stonyx/log';
3
- import { waitForModule } from 'stonyx';
4
- import { setup, emit } from '@stonyx/events';
5
- import RestServer from '@stonyx/rest-server';
6
- import TokenManager from './token-manager.js';
7
- import SessionManager from './session-manager.js';
8
- import AuthRequest from './auth-request.js';
9
-
10
- setup(['authenticate']);
11
-
12
- export default class OAuth {
13
- providers = new Map();
14
- pendingStates = new Map();
15
-
16
- constructor() {
17
- if (OAuth.instance) return OAuth.instance;
18
- OAuth.instance = this;
19
- }
20
-
21
- async init() {
22
- const { providers, sessionDuration, frontendCallbackUrl } = config.oauth;
23
- this.frontendCallbackUrl = frontendCallbackUrl;
24
-
25
- for (const [name, providerConfig] of Object.entries(providers)) {
26
- const modulePath = providerConfig.module
27
- ? `${config.rootPath}/${providerConfig.module}`
28
- : `./providers/${name}.js`;
29
- const { default: Provider } = await import(modulePath);
30
- const flow = new Provider(providerConfig);
31
- this.providers.set(name, { flow, tokenManager: new TokenManager(flow) });
32
- }
33
-
34
- this.sessionManager = new SessionManager(sessionDuration);
35
-
36
- await waitForModule('rest-server');
37
- RestServer.instance.mountRoute(AuthRequest, { name: 'auth', options: this });
38
-
39
- log.oauth?.('OAuth module initialized');
40
- }
41
-
42
- getProvider(name) {
43
- const provider = this.providers.get(name);
44
- if (!provider) throw new Error(`OAuth provider "${name}" is not configured`);
45
- return provider;
46
- }
47
-
48
- getAuthorizationUrl(providerName) {
49
- const { flow } = this.getProvider(providerName);
50
- const stateToken = crypto.randomUUID();
51
- this.pendingStates.set(stateToken, Date.now());
52
- return flow.buildAuthorizationUrl(stateToken);
53
- }
54
-
55
- async handleCallback(providerName, code, stateToken) {
56
- if (!stateToken || !this.pendingStates.has(stateToken)) {
57
- throw new Error('Invalid or missing state token');
58
- }
59
-
60
- const stateCreatedAt = this.pendingStates.get(stateToken);
61
- this.pendingStates.delete(stateToken);
62
-
63
- const TEN_MINUTES = 10 * 60 * 1000;
64
- if (Date.now() - stateCreatedAt > TEN_MINUTES) {
65
- throw new Error('State token has expired');
66
- }
67
-
68
- const { flow, tokenManager } = this.getProvider(providerName);
69
- const tokens = await tokenManager.getTokens(code);
70
- const rawUser = await flow.fetchUserInfo(tokens.accessToken);
71
- const user = flow.normalizeUser(rawUser);
72
- await emit('authenticate', user);
73
- return this.sessionManager.create(user, tokens);
74
- }
75
-
76
- getSession(sessionId) {
77
- return this.sessionManager.validate(sessionId);
78
- }
79
-
80
- logout(sessionId) {
81
- this.sessionManager.destroy(sessionId);
82
- }
83
- }
@@ -1,26 +0,0 @@
1
- export default class TokenManager {
2
- constructor(flow) {
3
- this.flow = flow;
4
- }
5
-
6
- async getTokens(code) {
7
- const tokens = await this.flow.exchangeCode(code);
8
- tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
9
- return tokens;
10
- }
11
-
12
- async refresh(refreshToken) {
13
- const tokens = await this.flow.refreshAccessToken(refreshToken);
14
- tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
15
- return tokens;
16
- }
17
-
18
- async revoke(accessToken) {
19
- return this.flow.revokeToken(accessToken);
20
- }
21
-
22
- isExpired(tokenData) {
23
- if (!tokenData?.expiresAt) return true;
24
- return Date.now() >= tokenData.expiresAt;
25
- }
26
- }
@@ -1,18 +0,0 @@
1
- export default {
2
- restServer: {
3
- dir: './test/sample/requests',
4
- },
5
- oauth: {
6
- providers: {
7
- mock: {
8
- clientId: 'test-client-id',
9
- clientSecret: 'test-client-secret',
10
- redirectUri: 'http://localhost:2666/auth/callback/mock',
11
- scopes: ['identify'],
12
- module: './test/sample/providers/mock.js',
13
- }
14
- },
15
- sessionDuration: 3600,
16
- frontendCallbackUrl: 'http://localhost:4200/auth/callback',
17
- }
18
- };