Skip to content

Commit 6ee4f08

Browse files
authored
Improved SAML auth handlers initialization (#570)
1 parent 8df81ae commit 6ee4f08

File tree

10 files changed

+114
-76
lines changed

10 files changed

+114
-76
lines changed

.env-cmdrc-template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"SWITCHER_GITOPS_URL": "http://localhost:8000"
4646
},
4747
"test": {
48+
"ENV": "TEST",
4849
"NODE_OPTIONS": "--experimental-vm-modules",
4950
"PORT": "3000",
5051
"MONGODB_URI": "mongodb://mongodb:27017/switcher-api-test",

.github/workflows/master.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ jobs:
1414

1515
steps:
1616
- name: Git checkout
17-
uses: actions/checkout@v4
17+
uses: actions/checkout@v5
1818
with:
1919
fetch-depth: 0
2020

2121
- name: Use Node.js 24.x
22-
uses: actions/setup-node@v4
22+
uses: actions/setup-node@v5
2323
with:
2424
node-version: 24.x
2525

@@ -102,12 +102,12 @@ jobs:
102102

103103
steps:
104104
- name: Checkout
105-
uses: actions/checkout@v4
105+
uses: actions/checkout@v5
106106
with:
107107
ref: 'master'
108108

109109
- name: Checkout Kustomize
110-
uses: actions/checkout@v4
110+
uses: actions/checkout@v5
111111
with:
112112
token: ${{ secrets.ARGOCD_PAT }}
113113
repository: switcherapi/switcher-deployment

.github/workflows/re-release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ jobs:
1515

1616
steps:
1717
- name: Git checkout
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v5
1919
with:
2020
fetch-depth: 0
2121
ref: ${{ github.event.inputs.tag }}
2222

2323
- name: Use Node.js 24.x
24-
uses: actions/setup-node@v4
24+
uses: actions/setup-node@v5
2525
with:
2626
node-version: 24.x
2727

@@ -68,7 +68,7 @@ jobs:
6868

6969
steps:
7070
- name: Checkout code
71-
uses: actions/checkout@v4
71+
uses: actions/checkout@v5
7272
with:
7373
fetch-depth: 0
7474
ref: ${{ github.event.inputs.tag }}

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111

1212
steps:
1313
- name: Git checkout
14-
uses: actions/checkout@v4
14+
uses: actions/checkout@v5
1515
with:
1616
fetch-depth: 0
1717

1818
- name: Use Node.js 24.x
19-
uses: actions/setup-node@v4
19+
uses: actions/setup-node@v5
2020
with:
2121
node-version: 24.x
2222

@@ -63,7 +63,7 @@ jobs:
6363

6464
steps:
6565
- name: Checkout code
66-
uses: actions/checkout@v4
66+
uses: actions/checkout@v5
6767

6868
- name: Docker meta
6969
id: meta

.github/workflows/sonar.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ jobs:
2828
core.setOutput('base_ref', pr.data.base.ref);
2929
core.setOutput('head_sha', pr.data.head.sha);
3030
31-
- uses: actions/checkout@v4
31+
- uses: actions/checkout@v5
3232
with:
3333
ref: ${{ steps.pr.outputs.head_sha }}
3434
fetch-depth: 0
3535

3636
- name: Use Node.js 24.x
37-
uses: actions/setup-node@v4
37+
uses: actions/setup-node@v5
3838
with:
3939
node-version: 24.x
4040

src/api-docs/paths/path-admin-saml.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export default {
88
responses: {
99
'302': {
1010
description: 'Redirect to SAML Identity Provider'
11+
},
12+
'404': {
13+
description: 'SAML not configured'
1114
}
1215
}
1316
}
@@ -37,6 +40,9 @@ export default {
3740
},
3841
'401': {
3942
description: 'SAML authentication failed'
43+
},
44+
'404': {
45+
description: 'SAML not configured'
4046
}
4147
}
4248
}
@@ -52,6 +58,12 @@ export default {
5258
'200': {
5359
description: 'Success',
5460
content: commonSchemaContent('AdminLoginResponse')
61+
},
62+
'401': {
63+
description: 'Authentication failed'
64+
},
65+
'404': {
66+
description: 'SAML not configured'
5567
}
5668
}
5769
}
@@ -71,6 +83,9 @@ export default {
7183
}
7284
}
7385
}
86+
},
87+
'404': {
88+
description: 'SAML not configured'
7489
}
7590
}
7691
}

src/app.js

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import './db/mongoose.js';
1111
import mongoose from 'mongoose';
1212
import swaggerDocument from './api-docs/swagger-document.js';
1313
import adminRouter from './routers/admin.js';
14-
import adminSamlRouter from './routers/admin-saml.js';
1514
import environment from './routers/environment.js';
1615
import component from './routers/component.js';
1716
import domainRouter from './routers/domain.js';
@@ -42,22 +41,23 @@ app.disable('x-powered-by');
4241
/**
4342
* Session configuration for SAML
4443
*/
45-
app.use(session({
46-
secret: process.env.SESSION_SECRET || 'switcher-api-session',
47-
resave: false,
48-
saveUninitialized: false,
49-
cookie: {
50-
secure: process.env.NODE_ENV === 'prod',
51-
maxAge: 24 * 60 * 60 * 1000 // 24 hours
52-
}
53-
}));
54-
app.use(passport.initialize());
44+
if (isSamlAvailable()) {
45+
app.use(session({
46+
secret: process.env.SESSION_SECRET,
47+
resave: false,
48+
saveUninitialized: false,
49+
cookie: {
50+
secure: true,
51+
maxAge: 5 * 60 * 1000 // 5 minutes
52+
}
53+
}));
54+
app.use(passport.initialize());
55+
}
5556

5657
/**
5758
* API Routes
5859
*/
5960
app.use(adminRouter);
60-
app.use(adminSamlRouter);
6161
app.use(component);
6262
app.use(environment);
6363
app.use(domainRouter);
@@ -70,6 +70,14 @@ app.use(permissionRouter);
7070
app.use(slackRouter);
7171
app.use(gitOpsRouter);
7272

73+
/**
74+
* SAML Routes
75+
*/
76+
if (isSamlAvailable()) {
77+
const adminSamlRouter = await import('./routers/admin-saml.js');
78+
app.use(adminSamlRouter.default);
79+
}
80+
7381
/**
7482
* GraphQL Routes
7583
*/
@@ -109,19 +117,38 @@ app.get('/check', defaultLimiter, (req, res) => {
109117
release_time: process.env.RELEASE_TIME,
110118
env: process.env.ENV,
111119
db_state: mongoose.connection.readyState,
112-
switcherapi: process.env.SWITCHER_API_ENABLE,
113-
switcherapi_logger: process.env.SWITCHER_API_LOGGER,
114-
relay_bypass_https: process.env.RELAY_BYPASS_HTTPS,
115-
relay_bypass_verification: process.env.RELAY_BYPASS_VERIFICATION,
116-
permission_cache: process.env.PERMISSION_CACHE_ACTIVATED,
117-
history: process.env.HISTORY_ACTIVATED,
120+
switcherapi: isEnabled('SWITCHER_API_ENABLE'),
121+
switcherapi_logger: isEnabled('SWITCHER_API_LOGGER'),
122+
relay_bypass_https: isEnabled('RELAY_BYPASS_HTTPS'),
123+
relay_bypass_verification: isEnabled('RELAY_BYPASS_VERIFICATION'),
124+
permission_cache: isEnabled('PERMISSION_CACHE_ACTIVATED'),
125+
history: isEnabled('HISTORY_ACTIVATED'),
118126
max_metrics_pages: process.env.METRICS_MAX_PAGE,
119127
max_stretegy_op: process.env.MAX_STRATEGY_OPERATION,
120-
max_rpm: process.env.MAX_REQUEST_PER_MINUTE || DEFAULT_RATE_LIMIT
128+
max_rpm: process.env.MAX_REQUEST_PER_MINUTE || DEFAULT_RATE_LIMIT,
129+
auth_providers: {
130+
saml: isSamlAvailable(),
131+
github: isOauthAvailableFor('GIT_OAUTH_CLIENT_ID', 'GIT_OAUTH_SECRET'),
132+
bitbucket: isOauthAvailableFor('BITBUCKET_OAUTH_CLIENT_ID', 'BITBUCKET_OAUTH_SECRET'),
133+
}
121134
};
122135
}
123136

124137
res.status(200).send(response);
125138
});
126139

140+
function isSamlAvailable() {
141+
return (process.env.SAML_ENTRY_POINT &&
142+
process.env.SAML_CALLBACK_ENDPOINT_URL &&
143+
process.env.SAML_CERT)?.length > 0;
144+
}
145+
146+
function isOauthAvailableFor(clientId, secret) {
147+
return (process.env[clientId] && process.env[secret])?.length > 0;
148+
}
149+
150+
function isEnabled(feature) {
151+
return process.env[feature] && process.env[feature].toLowerCase() === 'true';
152+
}
153+
127154
export default createServer(app);

src/external/saml.js

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,42 @@ import passport from 'passport';
33
import { signUpSaml } from '../services/admin.js';
44
import Logger from '../helpers/logger.js';
55

6-
function isSamlAvailable() {
7-
return process.env.SAML_ENTRY_POINT && process.env.SAML_CALLBACK_ENDPOINT_URL && process.env.SAML_CERT;
8-
}
6+
const samlOptions = {
7+
entryPoint: process.env.SAML_ENTRY_POINT,
8+
issuer: process.env.SAML_ISSUER || 'switcher-api',
9+
callbackUrl: `${process.env.SAML_CALLBACK_ENDPOINT_URL}/admin/saml/callback`,
10+
idpCert: Buffer.from(process.env.SAML_CERT, 'base64').toString('utf8'),
11+
privateKey: process.env.SAML_PRIVATE_KEY ? Buffer.from(process.env.SAML_PRIVATE_KEY, 'base64').toString('utf8') : undefined,
12+
identifierFormat: process.env.SAML_IDENTIFIER_FORMAT || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
13+
acceptedClockSkewMs: process.env.SAML_ACCEPTED_CLOCK_SKEW_MS ? parseInt(process.env.SAML_ACCEPTED_CLOCK_SKEW_MS, 10) : 5000,
14+
signatureAlgorithm: 'sha256',
15+
digestAlgorithm: 'sha256',
16+
wantAssertionsSigned: true,
17+
wantAuthnResponseSigned: false,
18+
};
919

10-
if (isSamlAvailable()) {
11-
const samlOptions = {
12-
entryPoint: process.env.SAML_ENTRY_POINT,
13-
issuer: process.env.SAML_ISSUER || 'switcher-api',
14-
callbackUrl: `${process.env.SAML_CALLBACK_ENDPOINT_URL}/admin/saml/callback`,
15-
idpCert: Buffer.from(process.env.SAML_CERT, 'base64').toString('utf8'),
16-
privateKey: process.env.SAML_PRIVATE_KEY ? Buffer.from(process.env.SAML_PRIVATE_KEY, 'base64').toString('utf8') : undefined,
17-
identifierFormat: process.env.SAML_IDENTIFIER_FORMAT || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
18-
acceptedClockSkewMs: process.env.SAML_ACCEPTED_CLOCK_SKEW_MS ? parseInt(process.env.SAML_ACCEPTED_CLOCK_SKEW_MS, 10) : 5000,
19-
signatureAlgorithm: 'sha256',
20-
digestAlgorithm: 'sha256',
21-
wantAssertionsSigned: true,
22-
wantAuthnResponseSigned: false,
23-
};
24-
25-
const samlStrategy = new SamlStrategy(samlOptions, async (profile, done) => {
26-
try {
27-
const userInfo = {
28-
id: profile.nameID,
29-
email: profile.email || profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
30-
name: profile.firstName || profile.nameID
31-
};
32-
33-
const { jwt } = await signUpSaml(userInfo);
34-
return done(null, { token: jwt.token });
35-
} catch (error) {
36-
Logger.error('SAML Strategy Error Event:', error);
37-
return done(error);
38-
}
39-
});
40-
41-
passport.use('saml', samlStrategy);
42-
Logger.info('SSO enabled: SAML strategy configured');
43-
Logger.info(` - Entry Point: ${samlOptions.entryPoint}`);
44-
Logger.info(` - Callback URL: ${samlOptions.callbackUrl}`);
45-
Logger.info(` - Issuer: ${samlOptions.issuer}`);
46-
Logger.info(` - Identifier Format: ${samlOptions.identifierFormat}`);
47-
Logger.info(` - Accepted Clock Skew (ms): ${samlOptions.acceptedClockSkewMs}`);
48-
Logger.info(` - Idp Cert: ${samlOptions.idpCert ? 'Provided' : 'Not Provided'}`);
49-
Logger.info(` - Private Key: ${samlOptions.privateKey ? 'Provided' : 'Not Provided'}`);
50-
}
20+
const samlStrategy = new SamlStrategy(samlOptions, async (profile, done) => {
21+
try {
22+
const userInfo = {
23+
id: profile.nameID,
24+
email: profile.email || profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
25+
name: profile.firstName || profile.nameID
26+
};
27+
28+
const { jwt } = await signUpSaml(userInfo);
29+
return done(null, { token: jwt.token });
30+
} catch (error) {
31+
Logger.error('SAML Strategy Error Event:', error);
32+
return done(error);
33+
}
34+
});
5135

36+
passport.use('saml', samlStrategy);
37+
Logger.info('SSO enabled: SAML strategy configured');
38+
Logger.info(` - Entry Point: ${samlOptions.entryPoint}`);
39+
Logger.info(` - Callback URL: ${samlOptions.callbackUrl}`);
40+
Logger.info(` - Issuer: ${samlOptions.issuer}`);
41+
Logger.info(` - Identifier Format: ${samlOptions.identifierFormat}`);
42+
Logger.info(` - Accepted Clock Skew (ms): ${samlOptions.acceptedClockSkewMs}`);
43+
Logger.info(` - Idp Cert: ${samlOptions.idpCert ? 'Provided' : 'Not Provided'}`);
44+
Logger.info(` - Private Key: ${samlOptions.privateKey ? 'Provided' : 'Not Provided'}`);

src/routers/admin-saml.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ router.get('/admin/saml/metadata', (_, res) => {
4242
const metadata = generateServiceProviderMetadata({
4343
issuer: process.env.SAML_ISSUER,
4444
callbackUrl: process.env.SAML_CALLBACK_URL,
45-
publicCerts: process.env.SAML_CERT
45+
publicCerts: Buffer.from(process.env.SAML_CERT, 'base64').toString('utf8'),
46+
privateKey: process.env.SAML_PRIVATE_KEY ? Buffer.from(process.env.SAML_PRIVATE_KEY, 'base64').toString('utf8') : undefined
4647
});
4748

4849
res.set('Content-Type', 'application/xml');

src/services/admin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export async function signUpSaml(userInfo) {
5959
let admin = await Admin.findUserBySamlId(userInfo.id);
6060
admin = await Admin.createThirdPartyAccount(
6161
admin, userInfo, 'saml', '_samlid', checkAdmin);
62+
6263
const jwt = await admin.generateAuthToken();
6364
return { admin, jwt };
6465
}

0 commit comments

Comments
 (0)