@soapjs/soap-auth 0.1.0
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/LICENSE +21 -0
- package/README.md +223 -0
- package/build/errors.d.ts +53 -0
- package/build/errors.js +117 -0
- package/build/factories/auth-strategy.factory.d.ts +9 -0
- package/build/factories/auth-strategy.factory.js +16 -0
- package/build/factories/http-auth-strategy.factory.d.ts +5 -0
- package/build/factories/http-auth-strategy.factory.js +42 -0
- package/build/factories/socket-auth-strategy.factory.d.ts +5 -0
- package/build/factories/socket-auth-strategy.factory.js +27 -0
- package/build/index.d.ts +28 -0
- package/build/index.js +44 -0
- package/build/session/file.session-store.d.ts +10 -0
- package/build/session/file.session-store.js +59 -0
- package/build/session/memory.session-store.d.ts +7 -0
- package/build/session/memory.session-store.js +19 -0
- package/build/session/session-handler.d.ts +17 -0
- package/build/session/session-handler.js +145 -0
- package/build/soap-auth.d.ts +16 -0
- package/build/soap-auth.js +75 -0
- package/build/strategies/api-key/api-key.errors.d.ts +9 -0
- package/build/strategies/api-key/api-key.errors.js +24 -0
- package/build/strategies/api-key/api-key.strategy.d.ts +14 -0
- package/build/strategies/api-key/api-key.strategy.js +95 -0
- package/build/strategies/api-key/api-key.types.d.ts +13 -0
- package/build/strategies/api-key/api-key.types.js +2 -0
- package/build/strategies/base-auth.strategy.d.ts +17 -0
- package/build/strategies/base-auth.strategy.js +69 -0
- package/build/strategies/basic/basic.strategy.d.ts +25 -0
- package/build/strategies/basic/basic.strategy.js +53 -0
- package/build/strategies/basic/basic.types.d.ts +10 -0
- package/build/strategies/basic/basic.types.js +2 -0
- package/build/strategies/credential-based-auth.strategy.d.ts +30 -0
- package/build/strategies/credential-based-auth.strategy.js +205 -0
- package/build/strategies/jwt/jwt.strategy.d.ts +10 -0
- package/build/strategies/jwt/jwt.strategy.js +69 -0
- package/build/strategies/jwt/jwt.tools.d.ts +3 -0
- package/build/strategies/jwt/jwt.tools.js +79 -0
- package/build/strategies/jwt/jwt.types.d.ts +33 -0
- package/build/strategies/jwt/jwt.types.js +2 -0
- package/build/strategies/local/local.strategy.d.ts +25 -0
- package/build/strategies/local/local.strategy.js +76 -0
- package/build/strategies/local/local.types.d.ts +3 -0
- package/build/strategies/local/local.types.js +2 -0
- package/build/strategies/oauth2/oauth2.strategy.d.ts +35 -0
- package/build/strategies/oauth2/oauth2.strategy.js +259 -0
- package/build/strategies/oauth2/oauth2.tools.d.ts +4 -0
- package/build/strategies/oauth2/oauth2.tools.js +22 -0
- package/build/strategies/oauth2/oauth2.types.d.ts +29 -0
- package/build/strategies/oauth2/oauth2.types.js +2 -0
- package/build/strategies/token-based-auth.strategy.d.ts +25 -0
- package/build/strategies/token-based-auth.strategy.js +124 -0
- package/build/tools/session.tools.d.ts +6 -0
- package/build/tools/session.tools.js +15 -0
- package/build/tools/token.tools.d.ts +7 -0
- package/build/tools/token.tools.js +32 -0
- package/build/tools/tools.d.ts +3 -0
- package/build/tools/tools.js +23 -0
- package/build/types.d.ts +251 -0
- package/build/types.js +2 -0
- package/jest.config.unit.json +10 -0
- package/ldap.md +62 -0
- package/package.json +33 -0
- package/saml.md +244 -0
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { LocalStrategyConfig } from "./strategies/local/local.types";
|
|
3
|
+
import { OAuth2StrategyConfig } from "./strategies/oauth2/oauth2.types";
|
|
4
|
+
import { ApiKeyStrategyConfig } from "./strategies/api-key/api-key.types";
|
|
5
|
+
import { BasicStrategyConfig } from "./strategies/basic/basic.types";
|
|
6
|
+
import { JwtConfig } from "./strategies/jwt/jwt.types";
|
|
7
|
+
export interface SessionStore {
|
|
8
|
+
getSession<T = SessionData>(sid: string, ...args: unknown[]): Promise<T | null>;
|
|
9
|
+
setSession<T = SessionData>(sid: string, session: T, ...args: unknown[]): void | Promise<void>;
|
|
10
|
+
destroySession(sid: string, ...args: unknown[]): void | Promise<void>;
|
|
11
|
+
touchSession<T = SessionData>(sid: string, session: T, ...args: unknown[]): void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export type SessionData = {
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
} | any;
|
|
16
|
+
export type SessionInfo<TData = any> = {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
data: TData;
|
|
19
|
+
};
|
|
20
|
+
export interface SessionConfig {
|
|
21
|
+
secret: string;
|
|
22
|
+
resave?: boolean;
|
|
23
|
+
saveUninitialized?: boolean;
|
|
24
|
+
store?: SessionStore;
|
|
25
|
+
sessionKey?: string;
|
|
26
|
+
sessionHeader?: string;
|
|
27
|
+
generateSessionId?: (...args: unknown[]) => string;
|
|
28
|
+
createSessionData?: <T>(data?: unknown, context?: unknown) => T;
|
|
29
|
+
embedSessionId?: (context: any, sessionId: string) => void;
|
|
30
|
+
getSessionId(context: any): string | null;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
export type Credentials = {
|
|
34
|
+
id: string;
|
|
35
|
+
hashedPassword: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
export interface AuthResultConfig<TContext = unknown, TUser = unknown> {
|
|
39
|
+
onSuccess?: (context: AuthSuccessContext<TUser, TContext>) => Promise<void> | void;
|
|
40
|
+
onFailure?: (context: AuthFailureContext<TContext>) => Promise<void> | void;
|
|
41
|
+
}
|
|
42
|
+
export interface SecurityConfig {
|
|
43
|
+
security?: {
|
|
44
|
+
maxFailedLoginAttempts?: number;
|
|
45
|
+
lockoutDuration?: number;
|
|
46
|
+
notifyOnLockout?: (account: any) => Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface BaseAuthStrategyConfig<TContext = unknown, TUser = unknown> extends AccountLockConfig<TContext>, RateLimitConfig, RoleAuthorizationConfig<TUser>, SecurityConfig {
|
|
50
|
+
mfa?: MfaConfig<TUser, TContext>;
|
|
51
|
+
session?: SessionConfig;
|
|
52
|
+
}
|
|
53
|
+
export interface AuditLoggingConfig<TContext = unknown> {
|
|
54
|
+
logAttempt?: (userId: string, success: boolean, context?: TContext) => Promise<void>;
|
|
55
|
+
logPasswordChange?: (userId: string, context?: TContext) => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface MfaConfig<TUser = unknown, TContext = unknown> {
|
|
58
|
+
extractMfaCode?: (context?: TContext) => string;
|
|
59
|
+
sendMfaCode?: (user: TUser, context?: TContext) => Promise<boolean>;
|
|
60
|
+
validateMfaCode?: (user: TUser, code: string) => Promise<boolean>;
|
|
61
|
+
isMfaRequired?: (user: TUser) => boolean;
|
|
62
|
+
generateMfaSecret?: (user: TUser) => Promise<string>;
|
|
63
|
+
verifyMfaSetup?: (user: TUser, secret: string) => Promise<boolean>;
|
|
64
|
+
disableMfa?: (user: TUser) => Promise<void>;
|
|
65
|
+
generateBackupCodes?: (user: TUser) => Promise<string[]>;
|
|
66
|
+
validateBackupCode?: (user: TUser, code: string) => Promise<boolean>;
|
|
67
|
+
maxMfaAttempts?: number;
|
|
68
|
+
lockMfaOnFailure?: (user: TUser) => Promise<void>;
|
|
69
|
+
getMfaAttempts?: (user: TUser) => Promise<number>;
|
|
70
|
+
resetMfaAttempts?: (user: TUser) => Promise<void>;
|
|
71
|
+
incrementMfaAttempts?: (user: TUser) => Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
export interface PasswordPolicyConfig {
|
|
74
|
+
validatePassword?: (password: string) => boolean;
|
|
75
|
+
getLastPasswordChange?: (identifier: string) => Date;
|
|
76
|
+
forcePasswordChangeOnFirstLogin?: boolean;
|
|
77
|
+
passwordExpirationDays?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface CredentialBasedAuthStrategyConfig<TContext = unknown, TUser = unknown> extends BaseAuthStrategyConfig<TContext, TUser> {
|
|
80
|
+
passwordPolicy?: PasswordPolicyConfig;
|
|
81
|
+
audit?: AuditLoggingConfig<TContext>;
|
|
82
|
+
logout: {} & AuthResultConfig<TContext, TUser>;
|
|
83
|
+
login: {
|
|
84
|
+
extractCredentials: (context: TContext) => Promise<{
|
|
85
|
+
identifier: string;
|
|
86
|
+
password: string;
|
|
87
|
+
}>;
|
|
88
|
+
retrieveUserData: (identifier: string) => Promise<TUser | null>;
|
|
89
|
+
verifyUserCredentials: (identifier: string, password: string) => Promise<boolean>;
|
|
90
|
+
incrementFailedAttempts?: (identifier: string) => Promise<void>;
|
|
91
|
+
resetFailedAttempts?: (identifier: string) => Promise<void>;
|
|
92
|
+
getFailedAttempts?: (identifier: string) => Promise<number>;
|
|
93
|
+
} & AuthResultConfig<TContext, TUser>;
|
|
94
|
+
passwordReset?: {
|
|
95
|
+
generateResetToken?: (identifier: string) => Promise<string>;
|
|
96
|
+
sendResetEmail?: (identifier: string, token: string) => Promise<void>;
|
|
97
|
+
validateResetToken?: (token: string) => Promise<boolean>;
|
|
98
|
+
updatePassword?: (identifier: string, newPassword: string) => Promise<void>;
|
|
99
|
+
} & AuthResultConfig<TContext, TUser>;
|
|
100
|
+
}
|
|
101
|
+
export interface TokenBasedAuthStrategyConfig<TContext = unknown, TUser = unknown> extends BaseAuthStrategyConfig<TContext, TUser>, TokenRotationConfig<TUser> {
|
|
102
|
+
tokens?: TokenHandlersConfig;
|
|
103
|
+
login: {
|
|
104
|
+
retrieveUserData: (identifier: string) => Promise<TUser | null>;
|
|
105
|
+
} & AuthResultConfig<TContext, TUser>;
|
|
106
|
+
logout: {} & AuthResultConfig<TContext, TUser>;
|
|
107
|
+
}
|
|
108
|
+
export interface TokenRotationConfig<TUser = unknown> {
|
|
109
|
+
enableRotation?: boolean;
|
|
110
|
+
maxRotations?: number;
|
|
111
|
+
storeUsedTokens?: boolean;
|
|
112
|
+
getRefreshToken?: (user: TUser) => Promise<string>;
|
|
113
|
+
rotateToken?: (refreshToken: string) => Promise<{
|
|
114
|
+
accessToken: string;
|
|
115
|
+
refreshToken: string;
|
|
116
|
+
}>;
|
|
117
|
+
onTokenRotated?: (user: TUser, newTokens: {
|
|
118
|
+
accessToken: string;
|
|
119
|
+
refreshToken: string;
|
|
120
|
+
}) => Promise<void>;
|
|
121
|
+
onTokenRotationFailure?: (user: TUser, error: Error) => Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
export interface AccountLockConfig<TContext = unknown> {
|
|
124
|
+
logFailedAttempt?: (account: any, context?: TContext) => Promise<void>;
|
|
125
|
+
lockAccount?: (account: any, ...args: unknown[]) => Promise<void>;
|
|
126
|
+
isAccountLocked?: (account: any, ...args: unknown[]) => Promise<boolean>;
|
|
127
|
+
}
|
|
128
|
+
export interface RateLimitConfig {
|
|
129
|
+
checkRateLimit?: (...args: unknown[]) => Promise<boolean>;
|
|
130
|
+
incrementRequestCount?: (...args: unknown[]) => Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
export interface RoleAuthorizationConfig<TUser = unknown> {
|
|
133
|
+
authorizeByRoles?: (user: TUser, roles: string[]) => Promise<boolean>;
|
|
134
|
+
roles?: string[];
|
|
135
|
+
}
|
|
136
|
+
export type AuthSuccessContext<TUser = unknown, TContext = unknown, TSessionData = unknown> = {
|
|
137
|
+
user?: TUser;
|
|
138
|
+
context?: TContext;
|
|
139
|
+
session?: SessionInfo<TSessionData>;
|
|
140
|
+
tokens?: string;
|
|
141
|
+
identifier?: string;
|
|
142
|
+
email?: string;
|
|
143
|
+
};
|
|
144
|
+
export type AuthFailureContext<TContext = unknown> = {
|
|
145
|
+
error: Error;
|
|
146
|
+
context?: TContext;
|
|
147
|
+
identifier?: string;
|
|
148
|
+
email?: string;
|
|
149
|
+
};
|
|
150
|
+
export type AuthResult<TUser, TSessionData = unknown> = {
|
|
151
|
+
user: TUser;
|
|
152
|
+
session?: SessionInfo<TSessionData>;
|
|
153
|
+
tokens?: {
|
|
154
|
+
accessToken?: string;
|
|
155
|
+
refreshToken?: string | null;
|
|
156
|
+
apiKey?: string | null;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
export interface AuthStrategy<TContext = unknown, TUser = unknown> {
|
|
160
|
+
init?(...args: unknown[]): Promise<void>;
|
|
161
|
+
authenticate(context?: TContext, ...args: unknown[]): Promise<AuthResult<TUser>>;
|
|
162
|
+
authorize?(user: any, action: string, resource?: string): Promise<boolean>;
|
|
163
|
+
refresh?(refreshToken: string): Promise<string>;
|
|
164
|
+
logout?(context?: TContext): Promise<void>;
|
|
165
|
+
}
|
|
166
|
+
export interface SoapHttpAuthConfig<TContext = unknown, TUser = unknown> {
|
|
167
|
+
local?: LocalStrategyConfig<TContext, TUser>;
|
|
168
|
+
oauth2?: {
|
|
169
|
+
[provider: string]: OAuth2StrategyConfig<TContext, TUser>;
|
|
170
|
+
};
|
|
171
|
+
apiKey?: ApiKeyStrategyConfig<TContext, TUser>;
|
|
172
|
+
jwt?: JwtConfig<TContext, TUser>;
|
|
173
|
+
basic?: BasicStrategyConfig<TContext, TUser>;
|
|
174
|
+
custom: {
|
|
175
|
+
[label: string]: AuthStrategy;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export interface SoapSocketAuthConfig<TContext = unknown, TUser = unknown> {
|
|
179
|
+
jwt?: JwtConfig<TContext, TUser>;
|
|
180
|
+
apiKey?: ApiKeyStrategyConfig<TContext, TUser>;
|
|
181
|
+
custom: {
|
|
182
|
+
[provider: string]: AuthStrategy;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export interface SoapAuthConfig<TContext = unknown, TUser = unknown> {
|
|
186
|
+
session?: SessionConfig;
|
|
187
|
+
tokens?: TokenHandlersConfig;
|
|
188
|
+
http?: SoapHttpAuthConfig<TContext, TUser>;
|
|
189
|
+
socket?: SoapSocketAuthConfig<TContext, TUser>;
|
|
190
|
+
logger?: Soap.Logger;
|
|
191
|
+
}
|
|
192
|
+
export interface CookieStorageOptions {
|
|
193
|
+
cookieName: string;
|
|
194
|
+
httpOnly: boolean;
|
|
195
|
+
secure: boolean;
|
|
196
|
+
sameSite?: "strict" | "lax" | "none";
|
|
197
|
+
maxAge?: number;
|
|
198
|
+
}
|
|
199
|
+
export interface HeaderStorageOptions {
|
|
200
|
+
headerName: string;
|
|
201
|
+
scheme?: string;
|
|
202
|
+
extractor: (scheme: string) => string | null;
|
|
203
|
+
}
|
|
204
|
+
export interface BodyStorageOptions {
|
|
205
|
+
name: string;
|
|
206
|
+
extractor: (name: string) => string | null;
|
|
207
|
+
}
|
|
208
|
+
export interface QueryStorageOptions {
|
|
209
|
+
name: string;
|
|
210
|
+
extractor: (name: string) => string | null;
|
|
211
|
+
}
|
|
212
|
+
export interface SessionStorageOptions {
|
|
213
|
+
name: string;
|
|
214
|
+
extractor: (name: string) => string | null;
|
|
215
|
+
}
|
|
216
|
+
export interface DatabaseOptions {
|
|
217
|
+
extractor: (...args: unknown[]) => Promise<string | null>;
|
|
218
|
+
}
|
|
219
|
+
export interface StorageContext {
|
|
220
|
+
storeInHeader?: (data: string, options?: HeaderStorageOptions) => Promise<void> | void;
|
|
221
|
+
storeInCookie?: (data: string, options?: CookieStorageOptions) => Promise<void> | void;
|
|
222
|
+
storeInSession?: (data: string, name?: string) => Promise<void> | void;
|
|
223
|
+
storeInBody?: (data: string, name?: string) => Promise<void> | void;
|
|
224
|
+
getFromHeader?: (options?: HeaderStorageOptions) => Promise<string | null> | string | null;
|
|
225
|
+
getFromCookie?: (cookieName: string) => Promise<string | undefined> | string | undefined;
|
|
226
|
+
getFromSession?: (name: string) => Promise<string | undefined> | string | undefined;
|
|
227
|
+
getFromBodyField?: (name: string) => Promise<string | undefined> | string | undefined;
|
|
228
|
+
removeFromCookie?: (cookieName: string) => Promise<void> | void;
|
|
229
|
+
removeFromSession?: (name: string) => Promise<void> | void;
|
|
230
|
+
encrypt?: (data: string) => Promise<string> | string;
|
|
231
|
+
decrypt?: (data: string) => Promise<string> | string;
|
|
232
|
+
}
|
|
233
|
+
export interface TokenHandlerConfig {
|
|
234
|
+
secretKey: string;
|
|
235
|
+
expiresIn: string | number;
|
|
236
|
+
audience?: string | string[];
|
|
237
|
+
issuer?: string | string[];
|
|
238
|
+
subject?: string;
|
|
239
|
+
tokenType?: string;
|
|
240
|
+
generate?: (payload: any) => string;
|
|
241
|
+
verify?: (token: string) => Promise<any>;
|
|
242
|
+
store?: (token: string, data: any, expiresIn: number) => Promise<void>;
|
|
243
|
+
retrieve?: (context: any) => Promise<string | null>;
|
|
244
|
+
remove?: (context: any) => Promise<void>;
|
|
245
|
+
embed?: (context: any, token: string) => void;
|
|
246
|
+
rotate?: (oldToken: string) => Promise<string>;
|
|
247
|
+
}
|
|
248
|
+
export interface TokenHandlersConfig {
|
|
249
|
+
access: TokenHandlerConfig;
|
|
250
|
+
refresh?: TokenHandlerConfig;
|
|
251
|
+
}
|
package/build/types.js
ADDED
package/ldap.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
## **LDAP Authentication Strategy - Example (WIP)**
|
|
2
|
+
|
|
3
|
+
The `LdapStrategy` allows authentication via an LDAP server. It extends the base `CredentialBasedAuthStrategy` and provides methods for user authentication, authorization, and session management.
|
|
4
|
+
|
|
5
|
+
### **Installation**
|
|
6
|
+
|
|
7
|
+
Before using the LDAP strategy, ensure you have installed the necessary package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ldapjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### **Example Usage**
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { LdapStrategy, LdapStrategyConfig } from "./strategies/ldap-strategy";
|
|
17
|
+
|
|
18
|
+
const ldapConfig: LdapStrategyConfig = {
|
|
19
|
+
ldapUrl: "ldap://ldap.example.com",
|
|
20
|
+
baseDn: "ou=users,dc=example,dc=com",
|
|
21
|
+
bindDn: "cn=admin,dc=example,dc=com",
|
|
22
|
+
bindCredentials: "adminpassword",
|
|
23
|
+
userFilter: "(uid={{identifier}})",
|
|
24
|
+
groupFilter: "(memberUid={{identifier}})",
|
|
25
|
+
mapUserAttributes: (entry) => ({
|
|
26
|
+
id: entry.uid,
|
|
27
|
+
fullName: entry.cn,
|
|
28
|
+
email: entry.mail,
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ldapAuth = new LdapStrategy(ldapConfig);
|
|
33
|
+
|
|
34
|
+
async function authenticateUser(username: string, password: string) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await ldapAuth.authenticate({ identifier: username, password });
|
|
37
|
+
console.log("User authenticated:", result.user);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Authentication failed:", error.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### **Features**
|
|
45
|
+
|
|
46
|
+
- Binds to the LDAP server using provided credentials.
|
|
47
|
+
- Retrieves user data based on LDAP search filters.
|
|
48
|
+
- Supports group membership verification (optional).
|
|
49
|
+
- Provides session management.
|
|
50
|
+
- Easily extendable and configurable.
|
|
51
|
+
|
|
52
|
+
### **Configuration Options**
|
|
53
|
+
|
|
54
|
+
| Option | Description | Required |
|
|
55
|
+
|-------------------|--------------------------------------------------------------|----------|
|
|
56
|
+
| `ldapUrl` | URL of the LDAP server | ✅ |
|
|
57
|
+
| `baseDn` | Base DN (Distinguished Name) for searching users | ✅ |
|
|
58
|
+
| `bindDn` | DN used to bind to the LDAP server | ✅ |
|
|
59
|
+
| `bindCredentials` | Password for the binding DN | ✅ |
|
|
60
|
+
| `userFilter` | LDAP filter to find users, e.g. `"(uid={{identifier}})"` | ✅ |
|
|
61
|
+
| `groupFilter` | (Optional) LDAP filter to check group membership | ❌ |
|
|
62
|
+
| `mapUserAttributes`| Function to map LDAP user attributes to application structure| ❌ |
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soapjs/soap-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"homepage": "https://docs.soapjs.com",
|
|
6
|
+
"repository": "https://github.com/soapjs/soap-auth",
|
|
7
|
+
"main": "build/index.js",
|
|
8
|
+
"types": "build/index.d.ts",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Radoslaw Kamysz",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test:unit": "jest --config=jest.config.unit.json",
|
|
13
|
+
"clean": "rm -rf ./build",
|
|
14
|
+
"build": "npm run clean && tsc -b",
|
|
15
|
+
"prepublish": "npm run clean && tsc --project tsconfig.build.json"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@soapjs/soap": "^0.5.2",
|
|
19
|
+
"@types/jest": "^27.0.3",
|
|
20
|
+
"jest": "^27.4.5",
|
|
21
|
+
"ts-jest": "^27.1.3",
|
|
22
|
+
"typescript": "^4.8.2"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@soapjs/soap": ">=0.5.2"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.7.9",
|
|
29
|
+
"bcrypt": "^5.1.1",
|
|
30
|
+
"jsonwebtoken": "^9.0.2",
|
|
31
|
+
"uuid": "^9.0.1"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/saml.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# **SAML Authentication Strategy (Google Workspace) - Example (WIP)**
|
|
2
|
+
|
|
3
|
+
This guide provides an example of implementing SAML authentication without Passport.js, using Google Workspace as the Identity Provider (IdP).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## **1. Installation**
|
|
8
|
+
|
|
9
|
+
Install the required dependencies:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install samlify xml-crypto express
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## **2. Configure Google Workspace (IdP)**
|
|
18
|
+
|
|
19
|
+
To set up Google as the Identity Provider (IdP):
|
|
20
|
+
|
|
21
|
+
1. **Sign in to Google Admin Console** ([admin.google.com](https://admin.google.com/)).
|
|
22
|
+
2. Navigate to **Apps > Web and Mobile Apps > Add App > Custom SAML App**.
|
|
23
|
+
3. Copy the following details from Google and save them in your project:
|
|
24
|
+
- **SSO URL** (Single Sign-On Service)
|
|
25
|
+
- **Entity ID** (Issuer)
|
|
26
|
+
- **X.509 Certificate** (for validating SAML assertions)
|
|
27
|
+
4. Configure the ACS URL (Assertion Consumer Service) to point to your application, e.g.:
|
|
28
|
+
```
|
|
29
|
+
https://your-app.com/auth/saml/callback
|
|
30
|
+
```
|
|
31
|
+
5. Map attributes such as:
|
|
32
|
+
- `email`
|
|
33
|
+
- `first_name`
|
|
34
|
+
- `last_name`
|
|
35
|
+
|
|
36
|
+
Save Google IdP metadata as `google-idp-metadata.xml` in your project.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## **3. Project Structure**
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
/saml-auth
|
|
44
|
+
├── config/
|
|
45
|
+
│ ├── google-idp-metadata.xml # Metadata from Google Admin
|
|
46
|
+
│ ├── sp-private-key.pem # Service provider private key
|
|
47
|
+
│ ├── sp-public-cert.pem # Service provider public certificate
|
|
48
|
+
├── src/
|
|
49
|
+
│ ├── saml-strategy.ts # SAML authentication strategy
|
|
50
|
+
│ ├── server.ts # Express server to handle requests
|
|
51
|
+
├── package.json
|
|
52
|
+
├── README.md
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## **4. Implementing the SAML Strategy**
|
|
58
|
+
|
|
59
|
+
### **saml-strategy.ts**
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import * as saml from "samlify";
|
|
63
|
+
import * as fs from "fs";
|
|
64
|
+
|
|
65
|
+
export class SAMLStrategy {
|
|
66
|
+
private serviceProvider: saml.ServiceProviderInstance;
|
|
67
|
+
private identityProvider: saml.IdentityProviderInstance;
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
// Load IdP metadata (from Google)
|
|
71
|
+
this.identityProvider = saml.IdentityProvider({
|
|
72
|
+
metadata: fs.readFileSync("./config/google-idp-metadata.xml", "utf-8"),
|
|
73
|
+
isAssertionEncrypted: false,
|
|
74
|
+
wantAuthnRequestsSigned: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Configure the Service Provider (our application)
|
|
78
|
+
this.serviceProvider = saml.ServiceProvider({
|
|
79
|
+
entityID: "https://your-app.com/metadata",
|
|
80
|
+
assertionConsumerService: [
|
|
81
|
+
{
|
|
82
|
+
Binding: saml.Constants.namespace.post,
|
|
83
|
+
Location: "https://your-app.com/auth/saml/callback",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
privateKey: fs.readFileSync("./config/sp-private-key.pem", "utf-8"),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generates SAML login URL to redirect users to Google Workspace SSO
|
|
92
|
+
* @returns {Promise<string>} SSO Login URL
|
|
93
|
+
*/
|
|
94
|
+
async getLoginUrl(): Promise<string> {
|
|
95
|
+
const { context } = await this.serviceProvider.createLoginRequest(
|
|
96
|
+
this.identityProvider,
|
|
97
|
+
"redirect"
|
|
98
|
+
);
|
|
99
|
+
return context; // SSO login URL for redirecting user
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handles SAML response from Google and extracts user info.
|
|
104
|
+
* @param {object} requestBody - The HTTP POST body containing SAMLResponse
|
|
105
|
+
* @returns {Promise<object>} Parsed user data
|
|
106
|
+
*/
|
|
107
|
+
async handleSamlResponse(requestBody: any): Promise<object> {
|
|
108
|
+
if (!requestBody.SAMLResponse) {
|
|
109
|
+
throw new Error("Missing SAML response.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { extract } = await this.serviceProvider.parseLoginResponse(
|
|
113
|
+
this.identityProvider,
|
|
114
|
+
"post",
|
|
115
|
+
requestBody
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
email: extract.attributes.email,
|
|
120
|
+
firstName: extract.attributes.first_name,
|
|
121
|
+
lastName: extract.attributes.last_name,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## **5. Handling authentication in Express.js**
|
|
130
|
+
|
|
131
|
+
### **server.ts**
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import express from "express";
|
|
135
|
+
import { SAMLStrategy } from "./saml-strategy";
|
|
136
|
+
|
|
137
|
+
const app = express();
|
|
138
|
+
const samlAuth = new SAMLStrategy();
|
|
139
|
+
|
|
140
|
+
app.get("/auth/saml/login", async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const loginUrl = await samlAuth.getLoginUrl();
|
|
143
|
+
res.redirect(loginUrl);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
res.status(500).send("Error generating login request.");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.post("/auth/saml/callback", express.urlencoded({ extended: false }), async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const userData = await samlAuth.handleSamlResponse(req.body);
|
|
152
|
+
res.json({ message: "Authentication successful", user: userData });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
res.status(401).send("Authentication failed");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
app.listen(3000, () => {
|
|
159
|
+
console.log("Server running on http://localhost:3000");
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## **6. Running the Application**
|
|
166
|
+
|
|
167
|
+
1. Start the server:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
node dist/server.js
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
2. Open the browser and go to:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
http://localhost:3000/auth/saml/login
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
You will be redirected to Google for login.
|
|
180
|
+
|
|
181
|
+
3. After successful login, you will be redirected to the callback URL, and your user information will be displayed.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## **7. Additional Features**
|
|
186
|
+
|
|
187
|
+
You can extend the SAML strategy with:
|
|
188
|
+
|
|
189
|
+
1. **Single Logout (SLO) Support**
|
|
190
|
+
- Implement an endpoint to handle logout requests from the IdP.
|
|
191
|
+
|
|
192
|
+
2. **Custom Attribute Mapping**
|
|
193
|
+
- Customize user attributes in the response (e.g. department, roles).
|
|
194
|
+
|
|
195
|
+
3. **Multi-IdP Support**
|
|
196
|
+
- Allow configuration for multiple SAML providers dynamically.
|
|
197
|
+
|
|
198
|
+
4. **Session Management**
|
|
199
|
+
- Store authenticated user session in Redis or JWT.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## **8. Environment Variables Example**
|
|
204
|
+
|
|
205
|
+
To keep sensitive data secure, use environment variables:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
SP_ENTITY_ID=https://your-app.com/metadata
|
|
209
|
+
SP_CALLBACK_URL=https://your-app.com/auth/saml/callback
|
|
210
|
+
SP_PRIVATE_KEY_PATH=./config/sp-private-key.pem
|
|
211
|
+
IDP_METADATA_PATH=./config/google-idp-metadata.xml
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
And access them in code like this:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import dotenv from "dotenv";
|
|
218
|
+
dotenv.config();
|
|
219
|
+
|
|
220
|
+
const spEntityId = process.env.SP_ENTITY_ID;
|
|
221
|
+
const callbackUrl = process.env.SP_CALLBACK_URL;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## **9. Security Considerations**
|
|
227
|
+
|
|
228
|
+
1. **Always validate the SAML response signature to prevent tampering.**
|
|
229
|
+
2. **Rotate the private keys regularly and store them securely.**
|
|
230
|
+
3. **Monitor SAML authentication logs for suspicious activity.**
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## **10. Conclusion**
|
|
235
|
+
|
|
236
|
+
This guide provided a simple yet powerful way to implement SAML authentication without relying on external frameworks like Passport.js. The setup allows easy integration with Google Workspace while ensuring flexibility and control over the authentication process.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### **11. Useful Resources**
|
|
241
|
+
|
|
242
|
+
- [Google Workspace SAML Setup](https://support.google.com/a/answer/6087519?hl=en)
|
|
243
|
+
- [samlify npm package](https://www.npmjs.com/package/samlify)
|
|
244
|
+
- [SAML Explained](https://www.okta.com/identity-101/what-is-saml/)
|