@hamak/auth 0.5.1

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 (116) hide show
  1. package/README.md +366 -0
  2. package/dist/api/api/auth-service.d.ts +111 -0
  3. package/dist/api/api/auth-service.d.ts.map +1 -0
  4. package/dist/api/api/auth-service.js +5 -0
  5. package/dist/api/api/index.d.ts +2 -0
  6. package/dist/api/api/index.d.ts.map +1 -0
  7. package/dist/api/api/index.js +1 -0
  8. package/dist/api/index.d.ts +10 -0
  9. package/dist/api/index.d.ts.map +1 -0
  10. package/dist/api/index.js +12 -0
  11. package/dist/api/tokens/index.d.ts +2 -0
  12. package/dist/api/tokens/index.d.ts.map +1 -0
  13. package/dist/api/tokens/index.js +1 -0
  14. package/dist/api/tokens/service-tokens.d.ts +26 -0
  15. package/dist/api/tokens/service-tokens.d.ts.map +1 -0
  16. package/dist/api/tokens/service-tokens.js +25 -0
  17. package/dist/api/types/auth-result.d.ts +69 -0
  18. package/dist/api/types/auth-result.d.ts.map +1 -0
  19. package/dist/api/types/auth-result.js +5 -0
  20. package/dist/api/types/config.d.ts +130 -0
  21. package/dist/api/types/config.d.ts.map +1 -0
  22. package/dist/api/types/config.js +5 -0
  23. package/dist/api/types/credentials.d.ts +52 -0
  24. package/dist/api/types/credentials.d.ts.map +1 -0
  25. package/dist/api/types/credentials.js +5 -0
  26. package/dist/api/types/index.d.ts +5 -0
  27. package/dist/api/types/index.d.ts.map +1 -0
  28. package/dist/api/types/index.js +4 -0
  29. package/dist/api/types/user.d.ts +39 -0
  30. package/dist/api/types/user.d.ts.map +1 -0
  31. package/dist/api/types/user.js +5 -0
  32. package/dist/impl/index.d.ts +15 -0
  33. package/dist/impl/index.d.ts.map +1 -0
  34. package/dist/impl/index.js +21 -0
  35. package/dist/impl/plugin/auth-plugin-factory.d.ts +20 -0
  36. package/dist/impl/plugin/auth-plugin-factory.d.ts.map +1 -0
  37. package/dist/impl/plugin/auth-plugin-factory.js +226 -0
  38. package/dist/impl/plugin/index.d.ts +2 -0
  39. package/dist/impl/plugin/index.d.ts.map +1 -0
  40. package/dist/impl/plugin/index.js +1 -0
  41. package/dist/impl/services/AuthService.d.ts +44 -0
  42. package/dist/impl/services/AuthService.d.ts.map +1 -0
  43. package/dist/impl/services/AuthService.js +277 -0
  44. package/dist/impl/services/index.d.ts +2 -0
  45. package/dist/impl/services/index.d.ts.map +1 -0
  46. package/dist/impl/services/index.js +1 -0
  47. package/dist/impl/storage/LocalTokenStorage.d.ts +32 -0
  48. package/dist/impl/storage/LocalTokenStorage.d.ts.map +1 -0
  49. package/dist/impl/storage/LocalTokenStorage.js +148 -0
  50. package/dist/impl/storage/MemoryTokenStorage.d.ts +34 -0
  51. package/dist/impl/storage/MemoryTokenStorage.d.ts.map +1 -0
  52. package/dist/impl/storage/MemoryTokenStorage.js +91 -0
  53. package/dist/impl/storage/SessionTokenStorage.d.ts +33 -0
  54. package/dist/impl/storage/SessionTokenStorage.d.ts.map +1 -0
  55. package/dist/impl/storage/SessionTokenStorage.js +147 -0
  56. package/dist/impl/storage/index.d.ts +10 -0
  57. package/dist/impl/storage/index.d.ts.map +1 -0
  58. package/dist/impl/storage/index.js +26 -0
  59. package/dist/impl/store/auth-reducer.d.ts +135 -0
  60. package/dist/impl/store/auth-reducer.d.ts.map +1 -0
  61. package/dist/impl/store/auth-reducer.js +179 -0
  62. package/dist/impl/store/index.d.ts +2 -0
  63. package/dist/impl/store/index.d.ts.map +1 -0
  64. package/dist/impl/store/index.js +1 -0
  65. package/dist/impl/strategies/KeycloakStrategy.d.ts +42 -0
  66. package/dist/impl/strategies/KeycloakStrategy.d.ts.map +1 -0
  67. package/dist/impl/strategies/KeycloakStrategy.js +237 -0
  68. package/dist/impl/strategies/OAuth2Strategy.d.ts +30 -0
  69. package/dist/impl/strategies/OAuth2Strategy.d.ts.map +1 -0
  70. package/dist/impl/strategies/OAuth2Strategy.js +232 -0
  71. package/dist/impl/strategies/PasswordStrategy.d.ts +25 -0
  72. package/dist/impl/strategies/PasswordStrategy.d.ts.map +1 -0
  73. package/dist/impl/strategies/PasswordStrategy.js +159 -0
  74. package/dist/impl/strategies/StrategyRegistry.d.ts +24 -0
  75. package/dist/impl/strategies/StrategyRegistry.d.ts.map +1 -0
  76. package/dist/impl/strategies/StrategyRegistry.js +70 -0
  77. package/dist/impl/strategies/index.d.ts +5 -0
  78. package/dist/impl/strategies/index.d.ts.map +1 -0
  79. package/dist/impl/strategies/index.js +4 -0
  80. package/dist/impl/utils/index.d.ts +3 -0
  81. package/dist/impl/utils/index.d.ts.map +1 -0
  82. package/dist/impl/utils/index.js +2 -0
  83. package/dist/impl/utils/jwt.d.ts +81 -0
  84. package/dist/impl/utils/jwt.d.ts.map +1 -0
  85. package/dist/impl/utils/jwt.js +103 -0
  86. package/dist/impl/utils/pkce.d.ts +44 -0
  87. package/dist/impl/utils/pkce.d.ts.map +1 -0
  88. package/dist/impl/utils/pkce.js +93 -0
  89. package/dist/index.d.ts +12 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +11 -0
  92. package/dist/spi/guards/AuthGuard.d.ts +108 -0
  93. package/dist/spi/guards/AuthGuard.d.ts.map +1 -0
  94. package/dist/spi/guards/AuthGuard.js +5 -0
  95. package/dist/spi/guards/index.d.ts +2 -0
  96. package/dist/spi/guards/index.d.ts.map +1 -0
  97. package/dist/spi/guards/index.js +1 -0
  98. package/dist/spi/index.d.ts +12 -0
  99. package/dist/spi/index.d.ts.map +1 -0
  100. package/dist/spi/index.js +15 -0
  101. package/dist/spi/storage/ITokenStorage.d.ts +107 -0
  102. package/dist/spi/storage/ITokenStorage.d.ts.map +1 -0
  103. package/dist/spi/storage/ITokenStorage.js +5 -0
  104. package/dist/spi/storage/index.d.ts +2 -0
  105. package/dist/spi/storage/index.d.ts.map +1 -0
  106. package/dist/spi/storage/index.js +1 -0
  107. package/dist/spi/strategies/IAuthStrategy.d.ts +114 -0
  108. package/dist/spi/strategies/IAuthStrategy.d.ts.map +1 -0
  109. package/dist/spi/strategies/IAuthStrategy.js +16 -0
  110. package/dist/spi/strategies/IStrategyRegistry.d.ts +64 -0
  111. package/dist/spi/strategies/IStrategyRegistry.d.ts.map +1 -0
  112. package/dist/spi/strategies/IStrategyRegistry.js +5 -0
  113. package/dist/spi/strategies/index.d.ts +3 -0
  114. package/dist/spi/strategies/index.d.ts.map +1 -0
  115. package/dist/spi/strategies/index.js +2 -0
  116. package/package.json +78 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Keycloak Authentication Strategy
3
+ * Implements authentication with Keycloak identity provider
4
+ */
5
+ import { OAuth2Strategy } from './OAuth2Strategy';
6
+ import { extractUserFromJWT } from '../utils/jwt';
7
+ /**
8
+ * Keycloak authentication strategy
9
+ *
10
+ * Extends OAuth2 strategy with Keycloak-specific features:
11
+ * - Direct grant (password) authentication
12
+ * - Role extraction from JWT tokens
13
+ * - Keycloak realm and resource roles
14
+ * - Proper logout with session invalidation
15
+ */
16
+ export class KeycloakStrategy {
17
+ constructor(config, httpClient, name = 'keycloak') {
18
+ this.config = config;
19
+ this.httpClient = httpClient;
20
+ this.type = 'keycloak';
21
+ this.name = name;
22
+ // Build Keycloak OpenID Connect URLs
23
+ this.baseUrl = `${config.serverUrl}/realms/${config.realm}/protocol/openid-connect`;
24
+ this.tokenUrl = `${this.baseUrl}/token`;
25
+ this.logoutUrl = `${this.baseUrl}/logout`;
26
+ // Create underlying OAuth2 strategy
27
+ this.oauth2 = new OAuth2Strategy({
28
+ clientId: config.clientId,
29
+ authorizationUrl: `${this.baseUrl}/auth`,
30
+ tokenUrl: this.tokenUrl,
31
+ userInfoUrl: `${this.baseUrl}/userinfo`,
32
+ redirectUri: config.redirectUri,
33
+ scope: config.scope || ['openid', 'profile', 'email'],
34
+ usePkce: config.usePkce !== false
35
+ }, httpClient, `${name}-oauth2`);
36
+ }
37
+ async getAuthorizationUrl() {
38
+ return this.oauth2.getAuthorizationUrl();
39
+ }
40
+ async handleCallback(params) {
41
+ const result = await this.oauth2.handleCallback(params);
42
+ if (result.success && result.accessToken) {
43
+ // Extract user with roles from JWT
44
+ result.user = this.extractUserWithRoles(result.accessToken);
45
+ }
46
+ return result;
47
+ }
48
+ async authenticate(credentials) {
49
+ // For password credentials, use direct grant
50
+ if (credentials.type === 'password' && this.config.enableDirectGrant) {
51
+ return this.authenticateDirect(credentials.username, credentials.password);
52
+ }
53
+ // For OAuth credentials, use callback flow
54
+ if (credentials.type === 'oauth') {
55
+ return this.handleCallback({
56
+ code: credentials.code,
57
+ state: credentials.state
58
+ });
59
+ }
60
+ return {
61
+ success: false,
62
+ error: {
63
+ code: 'invalid_credentials',
64
+ message: 'Invalid credentials type for Keycloak strategy'
65
+ }
66
+ };
67
+ }
68
+ async authenticateDirect(username, password) {
69
+ if (!this.config.enableDirectGrant) {
70
+ return {
71
+ success: false,
72
+ error: {
73
+ code: 'invalid_credentials',
74
+ message: 'Direct grant is not enabled for this Keycloak client'
75
+ }
76
+ };
77
+ }
78
+ try {
79
+ const response = await this.httpClient.post(this.tokenUrl, new URLSearchParams({
80
+ grant_type: 'password',
81
+ client_id: this.config.clientId,
82
+ username,
83
+ password,
84
+ scope: (this.config.scope || ['openid', 'profile', 'email']).join(' ')
85
+ }).toString(), {
86
+ headers: {
87
+ 'Content-Type': 'application/x-www-form-urlencoded'
88
+ }
89
+ });
90
+ const data = response.data;
91
+ const user = this.extractUserWithRoles(data.access_token);
92
+ return {
93
+ success: true,
94
+ user,
95
+ accessToken: data.access_token,
96
+ refreshToken: data.refresh_token,
97
+ tokenType: data.token_type || 'Bearer',
98
+ expiresAt: data.expires_in
99
+ ? Date.now() + data.expires_in * 1000
100
+ : undefined,
101
+ scope: data.scope?.split(' ')
102
+ };
103
+ }
104
+ catch (error) {
105
+ return this.handleKeycloakError(error);
106
+ }
107
+ }
108
+ async refreshToken(refreshToken) {
109
+ try {
110
+ const response = await this.httpClient.post(this.tokenUrl, new URLSearchParams({
111
+ grant_type: 'refresh_token',
112
+ client_id: this.config.clientId,
113
+ refresh_token: refreshToken
114
+ }).toString(), {
115
+ headers: {
116
+ 'Content-Type': 'application/x-www-form-urlencoded'
117
+ }
118
+ });
119
+ const data = response.data;
120
+ const user = this.extractUserWithRoles(data.access_token);
121
+ return {
122
+ success: true,
123
+ user,
124
+ accessToken: data.access_token,
125
+ refreshToken: data.refresh_token,
126
+ tokenType: data.token_type || 'Bearer',
127
+ expiresAt: data.expires_in
128
+ ? Date.now() + data.expires_in * 1000
129
+ : undefined,
130
+ scope: data.scope?.split(' ')
131
+ };
132
+ }
133
+ catch (error) {
134
+ return this.handleKeycloakError(error);
135
+ }
136
+ }
137
+ async logout(accessToken) {
138
+ try {
139
+ // Keycloak requires the refresh token for proper logout
140
+ // If we don't have it, we can still try to logout with just the access token
141
+ await this.httpClient.post(this.logoutUrl, new URLSearchParams({
142
+ client_id: this.config.clientId
143
+ }).toString(), {
144
+ headers: {
145
+ 'Content-Type': 'application/x-www-form-urlencoded',
146
+ Authorization: `Bearer ${accessToken}`
147
+ }
148
+ });
149
+ }
150
+ catch {
151
+ // Ignore logout errors - continue with local cleanup
152
+ }
153
+ this.oauth2.clearOAuthState();
154
+ }
155
+ /**
156
+ * Logout with refresh token for complete session invalidation
157
+ */
158
+ async logoutWithRefreshToken(refreshToken) {
159
+ try {
160
+ await this.httpClient.post(this.logoutUrl, new URLSearchParams({
161
+ client_id: this.config.clientId,
162
+ refresh_token: refreshToken
163
+ }).toString(), {
164
+ headers: {
165
+ 'Content-Type': 'application/x-www-form-urlencoded'
166
+ }
167
+ });
168
+ }
169
+ catch {
170
+ // Ignore logout errors
171
+ }
172
+ this.oauth2.clearOAuthState();
173
+ }
174
+ getStoredState() {
175
+ return this.oauth2.getStoredState();
176
+ }
177
+ clearOAuthState() {
178
+ this.oauth2.clearOAuthState();
179
+ }
180
+ extractUserFromToken(token) {
181
+ return this.extractUserWithRoles(token) || null;
182
+ }
183
+ extractUserWithRoles(token) {
184
+ const user = extractUserFromJWT(token, this.config.clientId);
185
+ return user || undefined;
186
+ }
187
+ handleKeycloakError(error) {
188
+ const httpError = error;
189
+ const errorCode = httpError.response?.data?.error;
190
+ const errorDesc = httpError.response?.data?.error_description;
191
+ const status = httpError.response?.status;
192
+ // Map Keycloak errors to our error codes
193
+ if (errorCode === 'invalid_grant') {
194
+ if (errorDesc?.includes('disabled')) {
195
+ return {
196
+ success: false,
197
+ error: {
198
+ code: 'account_disabled',
199
+ message: 'User account is disabled'
200
+ }
201
+ };
202
+ }
203
+ if (errorDesc?.includes('locked')) {
204
+ return {
205
+ success: false,
206
+ error: {
207
+ code: 'account_locked',
208
+ message: 'User account is locked'
209
+ }
210
+ };
211
+ }
212
+ return {
213
+ success: false,
214
+ error: {
215
+ code: 'invalid_credentials',
216
+ message: errorDesc || 'Invalid username or password'
217
+ }
218
+ };
219
+ }
220
+ if (status === 401) {
221
+ return {
222
+ success: false,
223
+ error: {
224
+ code: 'unauthorized',
225
+ message: errorDesc || 'Authentication failed'
226
+ }
227
+ };
228
+ }
229
+ return {
230
+ success: false,
231
+ error: {
232
+ code: 'server_error',
233
+ message: errorDesc || 'Keycloak authentication failed'
234
+ }
235
+ };
236
+ }
237
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * OAuth2 Authentication Strategy
3
+ * Implements OAuth2 Authorization Code flow with PKCE
4
+ */
5
+ import type { AuthResult, LoginCredentials, OAuth2StrategyConfig, OAuthCallbackParams } from '../../api';
6
+ import type { IOAuthStrategy, IHttpClient } from '../../spi';
7
+ /**
8
+ * OAuth2 authentication strategy
9
+ *
10
+ * Implements the OAuth2 Authorization Code flow with PKCE support.
11
+ * Can be used with any OAuth2-compliant identity provider.
12
+ */
13
+ export declare class OAuth2Strategy implements IOAuthStrategy {
14
+ private readonly config;
15
+ private readonly httpClient;
16
+ readonly type: "oauth2";
17
+ readonly name: string;
18
+ private storedState;
19
+ constructor(config: OAuth2StrategyConfig, httpClient: IHttpClient, name?: string);
20
+ getAuthorizationUrl(): Promise<string>;
21
+ handleCallback(params: OAuthCallbackParams): Promise<AuthResult>;
22
+ authenticate(credentials: LoginCredentials): Promise<AuthResult>;
23
+ refreshToken(refreshToken: string): Promise<AuthResult>;
24
+ logout(_accessToken: string): Promise<void>;
25
+ getStoredState(): string | null;
26
+ clearOAuthState(): void;
27
+ private fetchUserInfo;
28
+ private handleError;
29
+ }
30
+ //# sourceMappingURL=OAuth2Strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OAuth2Strategy.d.ts","sourceRoot":"","sources":["../../../src/impl/strategies/OAuth2Strategy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAqC7D;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,cAAc;IAMjD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN7B,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,WAAW,CAAuB;gBAGvB,MAAM,EAAE,oBAAoB,EAC5B,UAAU,EAAE,WAAW,EACxC,IAAI,SAAW;IAKX,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC;IAsCtC,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAgFhE,YAAY,CAAC,WAAW,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBhE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAuCvD,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjD,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B,eAAe,IAAI,IAAI;YAKT,aAAa;IA+B3B,OAAO,CAAC,WAAW;CAiCpB"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * OAuth2 Authentication Strategy
3
+ * Implements OAuth2 Authorization Code flow with PKCE
4
+ */
5
+ import { generateState, generateCodeVerifier, generateCodeChallenge, storePKCEState, retrievePKCEState, clearPKCEState } from '../utils/pkce';
6
+ /**
7
+ * OAuth2 authentication strategy
8
+ *
9
+ * Implements the OAuth2 Authorization Code flow with PKCE support.
10
+ * Can be used with any OAuth2-compliant identity provider.
11
+ */
12
+ export class OAuth2Strategy {
13
+ constructor(config, httpClient, name = 'oauth2') {
14
+ this.config = config;
15
+ this.httpClient = httpClient;
16
+ this.type = 'oauth2';
17
+ this.storedState = null;
18
+ this.name = name;
19
+ }
20
+ async getAuthorizationUrl() {
21
+ const state = generateState();
22
+ this.storedState = state;
23
+ const params = new URLSearchParams({
24
+ client_id: this.config.clientId,
25
+ redirect_uri: this.config.redirectUri,
26
+ response_type: this.config.responseType || 'code',
27
+ scope: this.config.scope.join(' '),
28
+ state
29
+ });
30
+ // Add PKCE if enabled (recommended for public clients)
31
+ if (this.config.usePkce !== false) {
32
+ const codeVerifier = generateCodeVerifier();
33
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
34
+ params.set('code_challenge', codeChallenge);
35
+ params.set('code_challenge_method', 'S256');
36
+ // Store PKCE state for callback
37
+ storePKCEState({
38
+ state,
39
+ codeVerifier,
40
+ createdAt: Date.now()
41
+ });
42
+ }
43
+ // Add any additional params
44
+ if (this.config.additionalParams) {
45
+ Object.entries(this.config.additionalParams).forEach(([key, value]) => {
46
+ params.set(key, value);
47
+ });
48
+ }
49
+ return `${this.config.authorizationUrl}?${params.toString()}`;
50
+ }
51
+ async handleCallback(params) {
52
+ // Check for OAuth errors
53
+ if (params.error) {
54
+ return {
55
+ success: false,
56
+ error: {
57
+ code: 'unauthorized',
58
+ message: params.errorDescription || params.error
59
+ }
60
+ };
61
+ }
62
+ // Validate state
63
+ const pkceState = retrievePKCEState();
64
+ if (pkceState && params.state !== pkceState.state) {
65
+ clearPKCEState();
66
+ return {
67
+ success: false,
68
+ error: {
69
+ code: 'invalid_state',
70
+ message: 'State parameter mismatch - possible CSRF attack'
71
+ }
72
+ };
73
+ }
74
+ try {
75
+ // Exchange code for tokens
76
+ const tokenParams = {
77
+ grant_type: 'authorization_code',
78
+ client_id: this.config.clientId,
79
+ code: params.code,
80
+ redirect_uri: this.config.redirectUri
81
+ };
82
+ // Add client secret if provided (for confidential clients)
83
+ if (this.config.clientSecret) {
84
+ tokenParams.client_secret = this.config.clientSecret;
85
+ }
86
+ // Add code verifier for PKCE
87
+ if (pkceState?.codeVerifier) {
88
+ tokenParams.code_verifier = pkceState.codeVerifier;
89
+ }
90
+ const response = await this.httpClient.post(this.config.tokenUrl, new URLSearchParams(tokenParams).toString(), {
91
+ headers: {
92
+ 'Content-Type': 'application/x-www-form-urlencoded'
93
+ }
94
+ });
95
+ clearPKCEState();
96
+ const data = response.data;
97
+ // Fetch user info
98
+ let user;
99
+ if (this.config.userInfoUrl) {
100
+ user = await this.fetchUserInfo(data.access_token);
101
+ }
102
+ return {
103
+ success: true,
104
+ user,
105
+ accessToken: data.access_token,
106
+ refreshToken: data.refresh_token,
107
+ tokenType: data.token_type || 'Bearer',
108
+ expiresAt: data.expires_in
109
+ ? Date.now() + data.expires_in * 1000
110
+ : undefined,
111
+ scope: data.scope?.split(' ')
112
+ };
113
+ }
114
+ catch (error) {
115
+ clearPKCEState();
116
+ return this.handleError(error);
117
+ }
118
+ }
119
+ async authenticate(credentials) {
120
+ if (credentials.type !== 'oauth') {
121
+ return {
122
+ success: false,
123
+ error: {
124
+ code: 'invalid_credentials',
125
+ message: 'Use handleCallback for OAuth authentication'
126
+ }
127
+ };
128
+ }
129
+ return this.handleCallback({
130
+ code: credentials.code,
131
+ state: credentials.state
132
+ });
133
+ }
134
+ async refreshToken(refreshToken) {
135
+ try {
136
+ const tokenParams = {
137
+ grant_type: 'refresh_token',
138
+ client_id: this.config.clientId,
139
+ refresh_token: refreshToken
140
+ };
141
+ if (this.config.clientSecret) {
142
+ tokenParams.client_secret = this.config.clientSecret;
143
+ }
144
+ const response = await this.httpClient.post(this.config.tokenUrl, new URLSearchParams(tokenParams).toString(), {
145
+ headers: {
146
+ 'Content-Type': 'application/x-www-form-urlencoded'
147
+ }
148
+ });
149
+ const data = response.data;
150
+ return {
151
+ success: true,
152
+ accessToken: data.access_token,
153
+ refreshToken: data.refresh_token,
154
+ tokenType: data.token_type || 'Bearer',
155
+ expiresAt: data.expires_in
156
+ ? Date.now() + data.expires_in * 1000
157
+ : undefined,
158
+ scope: data.scope?.split(' ')
159
+ };
160
+ }
161
+ catch (error) {
162
+ return this.handleError(error);
163
+ }
164
+ }
165
+ async logout(_accessToken) {
166
+ // OAuth2 doesn't have a standard logout endpoint
167
+ // Subclasses (like KeycloakStrategy) can override this
168
+ clearPKCEState();
169
+ }
170
+ getStoredState() {
171
+ return this.storedState || retrievePKCEState()?.state || null;
172
+ }
173
+ clearOAuthState() {
174
+ this.storedState = null;
175
+ clearPKCEState();
176
+ }
177
+ async fetchUserInfo(accessToken) {
178
+ if (!this.config.userInfoUrl) {
179
+ return undefined;
180
+ }
181
+ try {
182
+ const response = await this.httpClient.get(this.config.userInfoUrl, {
183
+ headers: {
184
+ Authorization: `Bearer ${accessToken}`
185
+ }
186
+ });
187
+ const data = response.data;
188
+ return {
189
+ id: data.sub,
190
+ email: data.email || '',
191
+ name: data.name || data.preferred_username,
192
+ username: data.preferred_username,
193
+ avatar: data.picture,
194
+ roles: [],
195
+ permissions: []
196
+ };
197
+ }
198
+ catch {
199
+ return undefined;
200
+ }
201
+ }
202
+ handleError(error) {
203
+ const httpError = error;
204
+ const errorCode = httpError.response?.data?.error;
205
+ const errorDesc = httpError.response?.data?.error_description;
206
+ if (errorCode === 'invalid_grant') {
207
+ return {
208
+ success: false,
209
+ error: {
210
+ code: 'invalid_token',
211
+ message: errorDesc || 'Invalid or expired authorization code'
212
+ }
213
+ };
214
+ }
215
+ if (errorCode === 'invalid_client') {
216
+ return {
217
+ success: false,
218
+ error: {
219
+ code: 'server_error',
220
+ message: 'Invalid client configuration'
221
+ }
222
+ };
223
+ }
224
+ return {
225
+ success: false,
226
+ error: {
227
+ code: 'server_error',
228
+ message: errorDesc || 'OAuth authentication failed'
229
+ }
230
+ };
231
+ }
232
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Password Authentication Strategy
3
+ * Implements username/password based authentication
4
+ */
5
+ import type { AuthResult, LoginCredentials, PasswordStrategyConfig } from '../../api';
6
+ import type { IAuthStrategy, IHttpClient } from '../../spi';
7
+ /**
8
+ * Password-based authentication strategy
9
+ *
10
+ * Authenticates users with username and password against a backend API.
11
+ * Supports token refresh and logout operations.
12
+ */
13
+ export declare class PasswordStrategy implements IAuthStrategy {
14
+ private readonly config;
15
+ private readonly httpClient;
16
+ readonly type: "password";
17
+ readonly name: string;
18
+ constructor(config: PasswordStrategyConfig, httpClient: IHttpClient, name?: string);
19
+ authenticate(credentials: LoginCredentials): Promise<AuthResult>;
20
+ refreshToken(refreshToken: string): Promise<AuthResult>;
21
+ logout(accessToken: string): Promise<void>;
22
+ private fetchUserInfo;
23
+ private handleError;
24
+ }
25
+ //# sourceMappingURL=PasswordStrategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasswordStrategy.d.ts","sourceRoot":"","sources":["../../../src/impl/strategies/PasswordStrategy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AA4B5D;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,aAAa;IAKlD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAL7B,QAAQ,CAAC,IAAI,EAAG,UAAU,CAAU;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAGH,MAAM,EAAE,sBAAsB,EAC9B,UAAU,EAAE,WAAW,EACxC,IAAI,SAAa;IAKb,YAAY,CAAC,WAAW,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA+ChE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA8BvD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAiBlC,aAAa;IAgC3B,OAAO,CAAC,WAAW;CA2CpB"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Password Authentication Strategy
3
+ * Implements username/password based authentication
4
+ */
5
+ /**
6
+ * Password-based authentication strategy
7
+ *
8
+ * Authenticates users with username and password against a backend API.
9
+ * Supports token refresh and logout operations.
10
+ */
11
+ export class PasswordStrategy {
12
+ constructor(config, httpClient, name = 'password') {
13
+ this.config = config;
14
+ this.httpClient = httpClient;
15
+ this.type = 'password';
16
+ this.name = name;
17
+ }
18
+ async authenticate(credentials) {
19
+ if (credentials.type !== 'password') {
20
+ return {
21
+ success: false,
22
+ error: {
23
+ code: 'invalid_credentials',
24
+ message: 'Invalid credentials type for password strategy'
25
+ }
26
+ };
27
+ }
28
+ try {
29
+ const response = await this.httpClient.post(this.config.loginEndpoint, {
30
+ username: credentials.username,
31
+ password: credentials.password
32
+ }, {
33
+ headers: this.config.headers
34
+ });
35
+ const data = response.data;
36
+ // Get user info if not included in login response
37
+ let user = data.user;
38
+ if (!user && this.config.userInfoEndpoint) {
39
+ user = await this.fetchUserInfo(data.access_token);
40
+ }
41
+ return {
42
+ success: true,
43
+ user,
44
+ accessToken: data.access_token,
45
+ refreshToken: data.refresh_token,
46
+ tokenType: data.token_type || 'Bearer',
47
+ expiresAt: data.expires_in
48
+ ? Date.now() + data.expires_in * 1000
49
+ : undefined,
50
+ scope: data.scope?.split(' ')
51
+ };
52
+ }
53
+ catch (error) {
54
+ return this.handleError(error);
55
+ }
56
+ }
57
+ async refreshToken(refreshToken) {
58
+ try {
59
+ const response = await this.httpClient.post(this.config.refreshEndpoint, {
60
+ refresh_token: refreshToken,
61
+ grant_type: 'refresh_token'
62
+ }, {
63
+ headers: this.config.headers
64
+ });
65
+ const data = response.data;
66
+ return {
67
+ success: true,
68
+ accessToken: data.access_token,
69
+ refreshToken: data.refresh_token,
70
+ tokenType: data.token_type || 'Bearer',
71
+ expiresAt: data.expires_in
72
+ ? Date.now() + data.expires_in * 1000
73
+ : undefined,
74
+ scope: data.scope?.split(' ')
75
+ };
76
+ }
77
+ catch (error) {
78
+ return this.handleError(error);
79
+ }
80
+ }
81
+ async logout(accessToken) {
82
+ try {
83
+ await this.httpClient.post(this.config.logoutEndpoint, {}, {
84
+ headers: {
85
+ ...this.config.headers,
86
+ Authorization: `Bearer ${accessToken}`
87
+ }
88
+ });
89
+ }
90
+ catch {
91
+ // Ignore logout errors - continue with local cleanup
92
+ }
93
+ }
94
+ async fetchUserInfo(accessToken) {
95
+ if (!this.config.userInfoEndpoint) {
96
+ return undefined;
97
+ }
98
+ try {
99
+ const response = await this.httpClient.get(this.config.userInfoEndpoint, {
100
+ headers: {
101
+ ...this.config.headers,
102
+ Authorization: `Bearer ${accessToken}`
103
+ }
104
+ });
105
+ const data = response.data;
106
+ return {
107
+ id: data.id,
108
+ email: data.email,
109
+ name: data.name,
110
+ username: data.username,
111
+ avatar: data.avatar,
112
+ roles: data.roles || [],
113
+ permissions: data.permissions || []
114
+ };
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ handleError(error) {
121
+ const httpError = error;
122
+ const status = httpError.response?.status;
123
+ const message = httpError.response?.data?.message;
124
+ if (status === 401) {
125
+ return {
126
+ success: false,
127
+ error: {
128
+ code: 'invalid_credentials',
129
+ message: message || 'Invalid username or password'
130
+ }
131
+ };
132
+ }
133
+ if (status === 403) {
134
+ return {
135
+ success: false,
136
+ error: {
137
+ code: 'forbidden',
138
+ message: message || 'Access forbidden'
139
+ }
140
+ };
141
+ }
142
+ if (status === 423) {
143
+ return {
144
+ success: false,
145
+ error: {
146
+ code: 'account_locked',
147
+ message: message || 'Account is locked'
148
+ }
149
+ };
150
+ }
151
+ return {
152
+ success: false,
153
+ error: {
154
+ code: 'server_error',
155
+ message: message || 'Authentication failed'
156
+ }
157
+ };
158
+ }
159
+ }