Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"passport-custom": "^1.1.1",
"passport-google-oauth20": "^2.0.0",
"passport-oauth2": "^1.5.0",
"passport-saml": "^2.1.0",
"prom-client": "^12.0.0",
"qs": "^6.9.4",
"queue": "^6.0.1",
Expand Down
1 change: 1 addition & 0 deletions server/src/auth/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './passport/gitlab';
import './passport/gitea';
import './passport/google';
import './passport/in-memory';
import './passport/saml';

if (authMethods.length === 0) {
throw new Error('No auth methods enabled, please configure one');
Expand Down
57 changes: 57 additions & 0 deletions server/src/auth/passport/saml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { env } from '../../env/env';
import passport from 'passport';
import { PassportUser } from '../create-or-update-user';
import chalk from 'chalk';
import { Strategy } from 'passport-saml';
import { Logger } from '../../commons/logger/logger';
import { authMethods } from './auth-methods';

const logger = new Logger('meli.api.passport:saml');

export const saml_redirect = '/auth/saml';
export const saml_callback = '/auth/saml/callback';

if (
env.MELI_SAML_ENDPOINT
&& env.MELI_SAML_ISSUER
) {
const samlCallbackUrl = `${env.MELI_URL}${saml_callback}`;
logger.debug('Enabling saml auth', samlCallbackUrl);

const stratOpts = {
path: saml_callback,
entryPoint: env.MELI_SAML_ENDPOINT,
issuer: env.MELI_SAML_ISSUER,
cert: null,
privateCert: null,
signatureAlgorithm: null,
};

if (
env.MELI_SAML_IDP_CRT
&& env.MELI_SAML_PRIVATE_CRT
) {
stratOpts.cert = env.MELI_SAML_IDP_CRT;
stratOpts.privateCert = env.MELI_SAML_PRIVATE_CRT;
stratOpts.signatureAlgorithm = 'sha256';
} else {
logger.warn(`You have not configured Meli to sign or validate requests. THIS IS INSECURE!
For more information, see https://docs.meli.sh/authentication/saml`);
}

passport.use(new Strategy(
stratOpts,
(profile, cb) => {
cb(undefined, <PassportUser>{
authProvider: 'saml',
id: profile.ID,
name: profile.nameID,
email: profile.email,
orgs: [],
});
},
));

logger.info(`Enabled ${chalk.green('saml')} auth`);
authMethods.push('saml');
}
3 changes: 3 additions & 0 deletions server/src/auth/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { github_callback, github_redirect } from './passport/github';
import { gitlab_callback, gitlab_redirect } from './passport/gitlab';
import { redirectToUi } from './handlers/redirect-to-ui';
import { noContent } from '../commons/express/handlers/no-content';
import { saml_callback, saml_redirect } from './passport/saml';

const router = Router();

Expand All @@ -34,6 +35,8 @@ router.get(google_redirect, passport.authenticate('google', googleOptions));
router.get(google_callback, passport.authenticate('google', passportOptions), authenticate, redirectToUi);
router.get(github_redirect, passport.authenticate('github'));
router.get(github_callback, passport.authenticate('github', passportOptions), authenticate, redirectToUi);
router.get(saml_redirect, passport.authenticate('saml', passportOptions));
router.post(saml_callback, passport.authenticate('saml', passportOptions), authenticate, redirectToUi);
router.post('/auth/in-memory', passport.authenticate('in-memory', passportOptions), authenticate, noContent);

// auth
Expand Down
12 changes: 12 additions & 0 deletions server/src/env/env-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,16 @@ export const envSpec: EnvSpec<Env> = {
MELI_GOOGLE_RECAPTCHA_SECRET_KEY: {
schema: string().optional(),
},
MELI_SAML_ENDPOINT: {
schema: string().optional(),
},
MELI_SAML_ISSUER: {
schema: string().optional(),
},
MELI_SAML_IDP_CRT: {
schema: string().optional(),
},
MELI_SAML_PRIVATE_CRT: {
schema: string().optional(),
},
};
5 changes: 4 additions & 1 deletion server/src/env/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export interface Env {
MELI_RESTRICTED_IPS: string[];
MELI_RESTRICTED_DOMAINS: string[];
MELI_CADDY_ADMIN_API_URL: string;
// Caddy content path
MELI_CADDY_DIR: string;
MELI_TMP_DIRECTORY: string;
MELI_SITES_DIR: string;
Expand All @@ -73,6 +72,10 @@ export interface Env {
MELI_MULTER_FORM_LIMITS: MulterLimitOptions;
MELI_GOOGLE_RECAPTCHA_SITE_KEY: string;
MELI_GOOGLE_RECAPTCHA_SECRET_KEY: string;
MELI_SAML_ENDPOINT: string;
MELI_SAML_ISSUER: string;
MELI_SAML_IDP_CRT: string;
MELI_SAML_PRIVATE_CRT: string;
}

export const env: Env = parseEnv(envSpec);
Expand Down
4 changes: 4 additions & 0 deletions ui/src/components/auth/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SignInWithGitlab } from './methods/SignInWithGitlab';
import { SignInWithGithub } from './methods/SignInWithGithub';
import { SignInWithGoogle } from './methods/SignInWithGoogle';
import { SignInWithUserPassword } from './methods/SignInWithUserPassword';
import { SignInWithSAML } from './methods/SignInWithSAML';

export function SignIn() {
const [loading, setLoading] = useMountedState(true);
Expand Down Expand Up @@ -57,6 +58,9 @@ export function SignIn() {
{signInMethods.includes('google') && (
<SignInWithGoogle/>
)}
{signInMethods.includes('saml') && (
<SignInWithSAML/>
)}
</div>
</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions ui/src/components/auth/methods/SignInWithSAML.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import '../../../styles/variables';

.saml {
background: $saml-bg;
}

.icon {
color: $saml-color;
display: block;
width: 100%;
}
27 changes: 27 additions & 0 deletions ui/src/components/auth/methods/SignInWithSAML.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey } from '@fortawesome/free-solid-svg-icons';
import React from 'react';
import styles from './SignInWithSAML.module.scss';
import { SignInButton } from './SignInButton';

export function SignInWithSAML({ className }: {
className?: any;
}) {
return (
<a
href="/auth/saml"
rel="noreferrer noopener"
className={className}
>
<SignInButton
icon={(
<div className={styles.icon}>
<FontAwesomeIcon icon={faKey} className="d-block w-100"/>
</div>
)}
label="Single Signon"
className={styles.SSO}
/>
</a>
);
}
2 changes: 2 additions & 0 deletions ui/src/styles/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ $github-color: #000000;
$github-bg: linear-gradient(250.75deg, #4d4d4d 0%, #000000 100%);
$google-color: #e8453c;
$google-bg: linear-gradient(250.75deg, #e8453c 0%, lighten(#e8453c, 10%) 100%);
$saml-color: #067BC2;
$saml-bg: linear-gradient(250.75deg, #067BC2 0%, lighten(#067BC2, 10%) 100%);

$drone-ci: linear-gradient(220.94deg, #0055CC 0%, #00347D 100%), #FFFFFF;
$circle-ci: linear-gradient(250.75deg, #461F93 0%, #1B0449 100%);
Expand Down