@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/build/errors.d.ts +53 -0
  4. package/build/errors.js +117 -0
  5. package/build/factories/auth-strategy.factory.d.ts +9 -0
  6. package/build/factories/auth-strategy.factory.js +16 -0
  7. package/build/factories/http-auth-strategy.factory.d.ts +5 -0
  8. package/build/factories/http-auth-strategy.factory.js +42 -0
  9. package/build/factories/socket-auth-strategy.factory.d.ts +5 -0
  10. package/build/factories/socket-auth-strategy.factory.js +27 -0
  11. package/build/index.d.ts +28 -0
  12. package/build/index.js +44 -0
  13. package/build/session/file.session-store.d.ts +10 -0
  14. package/build/session/file.session-store.js +59 -0
  15. package/build/session/memory.session-store.d.ts +7 -0
  16. package/build/session/memory.session-store.js +19 -0
  17. package/build/session/session-handler.d.ts +17 -0
  18. package/build/session/session-handler.js +145 -0
  19. package/build/soap-auth.d.ts +16 -0
  20. package/build/soap-auth.js +75 -0
  21. package/build/strategies/api-key/api-key.errors.d.ts +9 -0
  22. package/build/strategies/api-key/api-key.errors.js +24 -0
  23. package/build/strategies/api-key/api-key.strategy.d.ts +14 -0
  24. package/build/strategies/api-key/api-key.strategy.js +95 -0
  25. package/build/strategies/api-key/api-key.types.d.ts +13 -0
  26. package/build/strategies/api-key/api-key.types.js +2 -0
  27. package/build/strategies/base-auth.strategy.d.ts +17 -0
  28. package/build/strategies/base-auth.strategy.js +69 -0
  29. package/build/strategies/basic/basic.strategy.d.ts +25 -0
  30. package/build/strategies/basic/basic.strategy.js +53 -0
  31. package/build/strategies/basic/basic.types.d.ts +10 -0
  32. package/build/strategies/basic/basic.types.js +2 -0
  33. package/build/strategies/credential-based-auth.strategy.d.ts +30 -0
  34. package/build/strategies/credential-based-auth.strategy.js +205 -0
  35. package/build/strategies/jwt/jwt.strategy.d.ts +10 -0
  36. package/build/strategies/jwt/jwt.strategy.js +69 -0
  37. package/build/strategies/jwt/jwt.tools.d.ts +3 -0
  38. package/build/strategies/jwt/jwt.tools.js +79 -0
  39. package/build/strategies/jwt/jwt.types.d.ts +33 -0
  40. package/build/strategies/jwt/jwt.types.js +2 -0
  41. package/build/strategies/local/local.strategy.d.ts +25 -0
  42. package/build/strategies/local/local.strategy.js +76 -0
  43. package/build/strategies/local/local.types.d.ts +3 -0
  44. package/build/strategies/local/local.types.js +2 -0
  45. package/build/strategies/oauth2/oauth2.strategy.d.ts +35 -0
  46. package/build/strategies/oauth2/oauth2.strategy.js +259 -0
  47. package/build/strategies/oauth2/oauth2.tools.d.ts +4 -0
  48. package/build/strategies/oauth2/oauth2.tools.js +22 -0
  49. package/build/strategies/oauth2/oauth2.types.d.ts +29 -0
  50. package/build/strategies/oauth2/oauth2.types.js +2 -0
  51. package/build/strategies/token-based-auth.strategy.d.ts +25 -0
  52. package/build/strategies/token-based-auth.strategy.js +124 -0
  53. package/build/tools/session.tools.d.ts +6 -0
  54. package/build/tools/session.tools.js +15 -0
  55. package/build/tools/token.tools.d.ts +7 -0
  56. package/build/tools/token.tools.js +32 -0
  57. package/build/tools/tools.d.ts +3 -0
  58. package/build/tools/tools.js +23 -0
  59. package/build/types.d.ts +251 -0
  60. package/build/types.js +2 -0
  61. package/jest.config.unit.json +10 -0
  62. package/ldap.md +62 -0
  63. package/package.json +33 -0
  64. package/saml.md +244 -0
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ {
2
+ "preset": "ts-jest",
3
+ "testEnvironment": "node",
4
+ "clearMocks": true,
5
+ "collectCoverage": true,
6
+ "coverageDirectory": "coverage",
7
+ "coverageProvider": "v8",
8
+ "testMatch": ["**/__tests__/**/*.test.ts"],
9
+ "testTimeout": 10000
10
+ }
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/)