@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.
- package/README.md +366 -0
- package/dist/api/api/auth-service.d.ts +111 -0
- package/dist/api/api/auth-service.d.ts.map +1 -0
- package/dist/api/api/auth-service.js +5 -0
- package/dist/api/api/index.d.ts +2 -0
- package/dist/api/api/index.d.ts.map +1 -0
- package/dist/api/api/index.js +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +12 -0
- package/dist/api/tokens/index.d.ts +2 -0
- package/dist/api/tokens/index.d.ts.map +1 -0
- package/dist/api/tokens/index.js +1 -0
- package/dist/api/tokens/service-tokens.d.ts +26 -0
- package/dist/api/tokens/service-tokens.d.ts.map +1 -0
- package/dist/api/tokens/service-tokens.js +25 -0
- package/dist/api/types/auth-result.d.ts +69 -0
- package/dist/api/types/auth-result.d.ts.map +1 -0
- package/dist/api/types/auth-result.js +5 -0
- package/dist/api/types/config.d.ts +130 -0
- package/dist/api/types/config.d.ts.map +1 -0
- package/dist/api/types/config.js +5 -0
- package/dist/api/types/credentials.d.ts +52 -0
- package/dist/api/types/credentials.d.ts.map +1 -0
- package/dist/api/types/credentials.js +5 -0
- package/dist/api/types/index.d.ts +5 -0
- package/dist/api/types/index.d.ts.map +1 -0
- package/dist/api/types/index.js +4 -0
- package/dist/api/types/user.d.ts +39 -0
- package/dist/api/types/user.d.ts.map +1 -0
- package/dist/api/types/user.js +5 -0
- package/dist/impl/index.d.ts +15 -0
- package/dist/impl/index.d.ts.map +1 -0
- package/dist/impl/index.js +21 -0
- package/dist/impl/plugin/auth-plugin-factory.d.ts +20 -0
- package/dist/impl/plugin/auth-plugin-factory.d.ts.map +1 -0
- package/dist/impl/plugin/auth-plugin-factory.js +226 -0
- package/dist/impl/plugin/index.d.ts +2 -0
- package/dist/impl/plugin/index.d.ts.map +1 -0
- package/dist/impl/plugin/index.js +1 -0
- package/dist/impl/services/AuthService.d.ts +44 -0
- package/dist/impl/services/AuthService.d.ts.map +1 -0
- package/dist/impl/services/AuthService.js +277 -0
- package/dist/impl/services/index.d.ts +2 -0
- package/dist/impl/services/index.d.ts.map +1 -0
- package/dist/impl/services/index.js +1 -0
- package/dist/impl/storage/LocalTokenStorage.d.ts +32 -0
- package/dist/impl/storage/LocalTokenStorage.d.ts.map +1 -0
- package/dist/impl/storage/LocalTokenStorage.js +148 -0
- package/dist/impl/storage/MemoryTokenStorage.d.ts +34 -0
- package/dist/impl/storage/MemoryTokenStorage.d.ts.map +1 -0
- package/dist/impl/storage/MemoryTokenStorage.js +91 -0
- package/dist/impl/storage/SessionTokenStorage.d.ts +33 -0
- package/dist/impl/storage/SessionTokenStorage.d.ts.map +1 -0
- package/dist/impl/storage/SessionTokenStorage.js +147 -0
- package/dist/impl/storage/index.d.ts +10 -0
- package/dist/impl/storage/index.d.ts.map +1 -0
- package/dist/impl/storage/index.js +26 -0
- package/dist/impl/store/auth-reducer.d.ts +135 -0
- package/dist/impl/store/auth-reducer.d.ts.map +1 -0
- package/dist/impl/store/auth-reducer.js +179 -0
- package/dist/impl/store/index.d.ts +2 -0
- package/dist/impl/store/index.d.ts.map +1 -0
- package/dist/impl/store/index.js +1 -0
- package/dist/impl/strategies/KeycloakStrategy.d.ts +42 -0
- package/dist/impl/strategies/KeycloakStrategy.d.ts.map +1 -0
- package/dist/impl/strategies/KeycloakStrategy.js +237 -0
- package/dist/impl/strategies/OAuth2Strategy.d.ts +30 -0
- package/dist/impl/strategies/OAuth2Strategy.d.ts.map +1 -0
- package/dist/impl/strategies/OAuth2Strategy.js +232 -0
- package/dist/impl/strategies/PasswordStrategy.d.ts +25 -0
- package/dist/impl/strategies/PasswordStrategy.d.ts.map +1 -0
- package/dist/impl/strategies/PasswordStrategy.js +159 -0
- package/dist/impl/strategies/StrategyRegistry.d.ts +24 -0
- package/dist/impl/strategies/StrategyRegistry.d.ts.map +1 -0
- package/dist/impl/strategies/StrategyRegistry.js +70 -0
- package/dist/impl/strategies/index.d.ts +5 -0
- package/dist/impl/strategies/index.d.ts.map +1 -0
- package/dist/impl/strategies/index.js +4 -0
- package/dist/impl/utils/index.d.ts +3 -0
- package/dist/impl/utils/index.d.ts.map +1 -0
- package/dist/impl/utils/index.js +2 -0
- package/dist/impl/utils/jwt.d.ts +81 -0
- package/dist/impl/utils/jwt.d.ts.map +1 -0
- package/dist/impl/utils/jwt.js +103 -0
- package/dist/impl/utils/pkce.d.ts +44 -0
- package/dist/impl/utils/pkce.d.ts.map +1 -0
- package/dist/impl/utils/pkce.js +93 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/spi/guards/AuthGuard.d.ts +108 -0
- package/dist/spi/guards/AuthGuard.d.ts.map +1 -0
- package/dist/spi/guards/AuthGuard.js +5 -0
- package/dist/spi/guards/index.d.ts +2 -0
- package/dist/spi/guards/index.d.ts.map +1 -0
- package/dist/spi/guards/index.js +1 -0
- package/dist/spi/index.d.ts +12 -0
- package/dist/spi/index.d.ts.map +1 -0
- package/dist/spi/index.js +15 -0
- package/dist/spi/storage/ITokenStorage.d.ts +107 -0
- package/dist/spi/storage/ITokenStorage.d.ts.map +1 -0
- package/dist/spi/storage/ITokenStorage.js +5 -0
- package/dist/spi/storage/index.d.ts +2 -0
- package/dist/spi/storage/index.d.ts.map +1 -0
- package/dist/spi/storage/index.js +1 -0
- package/dist/spi/strategies/IAuthStrategy.d.ts +114 -0
- package/dist/spi/strategies/IAuthStrategy.d.ts.map +1 -0
- package/dist/spi/strategies/IAuthStrategy.js +16 -0
- package/dist/spi/strategies/IStrategyRegistry.d.ts +64 -0
- package/dist/spi/strategies/IStrategyRegistry.d.ts.map +1 -0
- package/dist/spi/strategies/IStrategyRegistry.js +5 -0
- package/dist/spi/strategies/index.d.ts +3 -0
- package/dist/spi/strategies/index.d.ts.map +1 -0
- package/dist/spi/strategies/index.js +2 -0
- 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
|
+
}
|