Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LDAP Authentication #792

Merged
merged 45 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
868faf6
Add backend for authenticating with LDAP
camdenmoors Jan 12, 2021
247cdef
Add frontend for LDAP login
camdenmoors Jan 21, 2021
f11288f
Undo changes to README
camdenmoors Jan 21, 2021
1626c99
Remove username requirement (interferes with regular login validation…
camdenmoors Jan 21, 2021
c1eb162
Merge branch 'master' into LDAP
Jan 21, 2021
60591ca
Fix sonarcloud recommendations
camdenmoors Jan 21, 2021
8695c05
Merge branch 'LDAP' of https://github.com/mitre/heimdall2 into LDAP
camdenmoors Jan 21, 2021
cb9ff31
constify ldap login creds
camdenmoors Jan 21, 2021
d079468
Split Local Login and LDAPLogin into two components
camdenmoors Jan 21, 2021
3d69145
Remove unused UserValidatorMixin
camdenmoors Jan 21, 2021
5e66a13
Constify locallogin creds
camdenmoors Jan 21, 2021
d123a29
Add openldap container to e2e job
camdenmoors Jan 22, 2021
5075125
Change container name and lint integration tests
camdenmoors Jan 22, 2021
b3aeec1
Merge branch 'master' into LDAP
camdenmoors Jan 22, 2021
86166ec
Remove container_name
camdenmoors Jan 22, 2021
37158cb
Merge branch 'LDAP' of https://github.com/mitre/heimdall2 into LDAP
camdenmoors Jan 22, 2021
5701415
Reformat env variables
camdenmoors Jan 22, 2021
e45c5ac
Seed user data
camdenmoors Jan 22, 2021
243c664
Try getting current directory using pwd
camdenmoors Jan 22, 2021
7a8c77f
Remove command:
camdenmoors Jan 22, 2021
2cb3ad9
Try using ${{ github.workspace }}
camdenmoors Jan 22, 2021
4e1c21b
Switch to using test-openldap
camdenmoors Jan 22, 2021
301e935
Merge branch 'master' into LDAP
camdenmoors Jan 25, 2021
5552784
Add cypress tests for ldap login
camdenmoors Jan 25, 2021
ee7652b
Create heimdallci network (trying to fix dns problems)
camdenmoors Jan 25, 2021
4468de6
Re-run without custom network
camdenmoors Jan 25, 2021
f9b92df
Remove heimdallci network
camdenmoors Jan 25, 2021
e4bb158
Print all environment variables (debug)
camdenmoors Jan 25, 2021
c3bda4f
Move LDAP configuration to .env-ci, remove debug
camdenmoors Jan 25, 2021
b6a039e
Set LDAP_HOST to localhost, newline EOF
camdenmoors Jan 25, 2021
49bb74f
Type loginToLDAP
camdenmoors Jan 26, 2021
f9bb87a
Allow Oauth/LDAP users to edit their profile without password
camdenmoors Jan 28, 2021
685beb2
Merge branch 'master' into LDAP
camdenmoors Jan 28, 2021
8e76c13
Add creationMethod to test constants
camdenmoors Jan 28, 2021
9b8cea8
Merge branch 'LDAP' of https://github.com/mitre/heimdall2 into LDAP
camdenmoors Jan 28, 2021
5a6b168
Add creationMethod to api doc, add creationMethod to all CREATE_USER …
camdenmoors Jan 28, 2021
01737c9
Remove debug
camdenmoors Jan 28, 2021
e4b8b6c
Remove all debug
camdenmoors Jan 28, 2021
3dfe211
Disabled changing user info for LDAP users, update user info on login
camdenmoors Jan 28, 2021
f2e7fa9
!= !=(?) !==
camdenmoors Jan 28, 2021
5cc2c64
Merge branch 'master' into LDAP
camdenmoors Jan 29, 2021
fc003a2
Merge branch 'master' into LDAP
camdenmoors Feb 1, 2021
0251fa0
Merge branch 'master' into LDAP
Feb 1, 2021
fe6d2b8
Fix identity
camdenmoors Feb 1, 2021
e68e4f5
Merge branch 'master' into LDAP
Feb 1, 2021
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
90 changes: 47 additions & 43 deletions .github/workflows/e2e-ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Run E2E Backend + Frontend Tests

on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]

jobs:
build:
Expand All @@ -19,54 +19,58 @@ jobs:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
ldap:
image: rroemhild/test-openldap
ports:
- 10389:10389

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2

- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-

- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: "14.x"

- name: Install project dependencies
run: yarn install --frozen-lockfile
- name: Install project dependencies
run: yarn install --frozen-lockfile

- name: Copy .env-ci to .env
run: cp apps/backend/test/.env-ci apps/backend/.env
- name: Copy .env-ci to .env
run: cp apps/backend/test/.env-ci apps/backend/.env

- name: Create/migrate db
run: |
yarn backend sequelize-cli db:create
yarn backend sequelize-cli db:migrate
yarn backend sequelize-cli db:seed:all
- name: Create/migrate db
run: |
yarn backend sequelize-cli db:create
yarn backend sequelize-cli db:migrate
yarn backend sequelize-cli db:seed:all

- name: Build Heimdall
run: yarn build
env:
NODE_ENV: production
- name: Build Heimdall
run: yarn build
env:
NODE_ENV: production

- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: yarn start
wait-on: http://127.0.0.1:3000
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: yarn start
wait-on: http://127.0.0.1:3000

- name: Upload test screenshots
if: failure()
uses: actions/upload-artifact@master
with:
name: cypress-screenshots
path: test/screenshots
- name: Upload test screenshots
if: failure()
uses: actions/upload-artifact@master
with:
name: cypress-screenshots
path: test/screenshots
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Proper API documentation does not exist yet. In the meantime here are quick inst

```sh
# Create a user (only needs to be done once)
curl -X POST -H "Content-Type: application/json" -d '{"email": "[email protected]", "password": "password", "passwordConfirmation": "password", "role": "user" }' http://localhost:3000/users
curl -X POST -H "Content-Type: application/json" -d '{"email": "[email protected]", "password": "password", "passwordConfirmation": "password", "role": "user", "creationMethod": "local" }' http://localhost:3000/users
# Log in
curl -X POST -H "Content-Type: application/json" -d '{"email": "[email protected]", "password": "password" }' http://localhost:3000/authn/login
# The previous command returns a Bearer Token that needs to get placed in the following command
Expand Down
15 changes: 15 additions & 0 deletions apps/backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,18 @@ JWT_EXPIRE_TIME=<JSON Web Token Length of time before signature expires (if noth
NODE_ENV=<development, production, or test (no default, must be set)>
HEIMDALL_HEADLESS_TESTS=<run integration tests in a headless browser (default=true)>
ADMIN_PASSWORD=<Password for admin user (if nothing is provided, defaults to a randomly generated password)>

# LDAP Configuration
LDAP_ENABLED=<If you want to enable LDAP login (true/false)>
LDAP_HOST=<Your LDAP target server>
LDAP_PORT=<Your LDAP target port (if nothing is provided, defaults to 389)>
LDAP_BINDDN=<The Dn of the user used for lookups>
LDAP_PASSWORD=<Your LDAP user's passwords used for lookups>
# Here you set your LDAP searchbase, for more info see https://docs.oracle.com/cd/E19693-01/819-0997/auto45/index.html
# If you're using Active Directory, you probably want "OU=Users, DC=<yourdomain>, DC=local"
LDAP_SEARCHBASE="<Your LDAP search base>"
# Here you set your LDAP search filter, for more info see https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html
# If you are using Active Directory Users, you probably want "sAMAccountName={{username}}"
LDAP_SEARCHFILTER="<Your LDAP search filter (if nothing is provided, defaults to (sAMAccountName={{username}})>"
LDAP_NAMEFIELD="<The field that contains the user's full name (if nothing is provided, defaults to name)>"
LDAP_MAILFIELD="<The field that contains the user's email (if nothing is provided, defaults to mail)>"
4 changes: 2 additions & 2 deletions apps/backend/config/app_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export default class AppConfig {
dialectOptions: {
ssl: Boolean(this.get('DATABASE_SSL'))
? {
rejectUnauthorized: false
}
rejectUnauthorized: false
}
: false
},
ssl: Boolean(this.get('DATABASE_SSL')) || false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const sequelize = require("sequelize");

module.exports = {
up: async (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.addColumn('Users', 'creationMethod', {
type: sequelize.STRING,
defaultValue: 'local'
})
])
})
},

down: async (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('Users', 'creationMethod', { transaction: t })
])
})
}
};
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"js-levenshtein": "^1.1.6",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0",
"pg": "^8.2.1",
"reflect-metadata": "^0.1.13",
Expand Down
13 changes: 12 additions & 1 deletion apps/backend/src/authn/authn.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Controller, Post, Req, UseGuards} from '@nestjs/common';
import {AuthGuard} from '@nestjs/passport';
import {Request} from 'express';
import {LocalAuthGuard} from '../guards/local-auth.guard';
import {User} from '../users/user.model';
Expand All @@ -10,7 +11,17 @@ export class AuthnController {

@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Req() req: Request): Promise<any> {
async login(
@Req() req: Request
): Promise<{userID: string; accessToken: string}> {
return this.authnService.login(req.user as User);
}

@UseGuards(AuthGuard('ldap'))
@Post('login/ldap')
async loginToLDAP(
@Req() req: Request
): Promise<{userID: string; accessToken: string}> {
return this.authnService.login(req.user as User);
}
}
3 changes: 2 additions & 1 deletion apps/backend/src/authn/authn.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {UsersModule} from '../users/users.module';
import {AuthnController} from './authn.controller';
import {AuthnService} from './authn.service';
import {JwtStrategy} from './jwt.strategy';
import {LDAPStrategy} from './ldap.strategy';
import {LocalStrategy} from './local.strategy';

@Module({
imports: [UsersModule, PassportModule, TokenModule, ConfigModule],
providers: [AuthnService, LocalStrategy, JwtStrategy],
providers: [AuthnService, LocalStrategy, JwtStrategy, LDAPStrategy],
controllers: [AuthnController]
})
export class AuthnModule {}
54 changes: 53 additions & 1 deletion apps/backend/src/authn/authn.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import {Injectable, UnauthorizedException} from '@nestjs/common';
import {JwtService} from '@nestjs/jwt';
import {compare} from 'bcrypt';
import * as crypto from 'crypto';
import {ConfigService} from '../config/config.service';
import {CreateUserDto} from '../users/dto/create-user.dto';
import {User} from '../users/user.model';
import {UsersService} from '../users/users.service';

@Injectable()
export class AuthnService {
constructor(
private usersService: UsersService,
private readonly configService: ConfigService,
private jwtService: JwtService
) {}

async validateUser(email: string, password: string): Promise<any> {
async validateUser(email: string, password: string): Promise<User | null> {
let user: User;
try {
user = await this.usersService.findByEmail(email);
Expand All @@ -26,6 +30,46 @@ export class AuthnService {
}
}

async validateOrCreateUser(
email: string,
firstName: string,
lastName: string,
creationMethod: string
): Promise<User> {
let user: User;
try {
user = await this.usersService.findByEmail(email);
} catch {
const randomPass = crypto.randomBytes(128).toString('hex');
const createUser: CreateUserDto = {
email: email,
password: randomPass,
passwordConfirmation: randomPass,
firstName: firstName,
lastName: lastName,
organization: '',
title: '',
role: 'user',
creationMethod: creationMethod
};
await this.usersService.create(createUser);
user = await this.usersService.findByEmail(email);
}

if (user) {
// If the users info has changed since they last logged in it will be reflected here.
// Because we find the user by their email, we can't detect a change in email.
if (user.firstName !== firstName || user.lastName !== lastName) {
user.firstName = firstName;
user.lastName = lastName;
user.save();
}
this.usersService.updateLoginMetadata(user);
}

return user;
}

async login(user: {
id: string;
email: string;
Expand All @@ -51,4 +95,12 @@ export class AuthnService {
};
}
}

splitName(fullName: string): {firstName: string; lastName: string} {
const nameArray = fullName.split(' ');
return {
firstName: nameArray.slice(0, -1).join(' '),
lastName: nameArray[nameArray.length - 1]
};
}
}
47 changes: 47 additions & 0 deletions apps/backend/src/authn/ldap.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Injectable} from '@nestjs/common';
import {PassportStrategy} from '@nestjs/passport';
import {Request} from 'express';
import Strategy from 'passport-ldapauth';
import {ConfigService} from '../config/config.service';
import {AuthnService} from './authn.service';

@Injectable()
export class LDAPStrategy extends PassportStrategy(Strategy, 'ldap') {
constructor(
private readonly authnService: AuthnService,
private readonly configService: ConfigService
) {
super(
{
passReqToCallback: true,
server: {
url: `ldap://${configService.get('LDAP_HOST')}:${
configService.get('LDAP_PORT') || 389
}`,
bindDN: configService.get('LDAP_BINDDN'),
bindCredentials: configService.get('LDAP_PASSWORD'),
searchBase: configService.get('LDAP_SEARCHBASE') || 'disabled',
searchFilter:
configService.get('LDAP_SEARCHFILTER') ||
'(sAMAccountName={{username}})',
passReqToCallback: true
}
},
async (req: Request, user: any, done: any) => {
const {firstName, lastName} = this.authnService.splitName(
user[configService.get('LDAP_NAMEFIELD') || 'name']
);
const email: string =
user[configService.get('LDAP_MAILFIELD') || 'mail'];

req.user = this.authnService.validateOrCreateUser(
email,
firstName,
lastName,
'ldap'
);
return done(null, req.user);
}
);
}
}
5 changes: 4 additions & 1 deletion apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export class ConfigService {
}

frontendStartupSettings(): StartupSettingsDto {
return new StartupSettingsDto({banner: this.get('WARNING_BANNER') || ''});
return new StartupSettingsDto({
banner: this.get('WARNING_BANNER') || '',
ldap: this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true' || false
});
}

getDbConfig(): SequelizeOptions {
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/config/dto/startup-settings.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {IStartupSettings} from '@heimdall/interfaces';

export class StartupSettingsDto implements IStartupSettings {
readonly banner: string;
readonly ldap: boolean;

constructor(settings: IStartupSettings) {
this.banner = settings.banner;
this.ldap = settings.ldap;
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ export class CreateUserDto implements ICreateUser {
@IsString()
@IsIn(['user'])
readonly role!: string;

@IsNotEmpty()
@IsString()
@IsIn(['local', 'ldap', 'oauth'])
readonly creationMethod!: string;
}
Loading