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 9 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
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)>"
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
7 changes: 7 additions & 0 deletions 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 @@ -13,4 +14,10 @@ export class AuthnController {
async login(@Req() req: Request): Promise<any> {
return this.authnService.login(req.user as User);
}

@UseGuards(AuthGuard('ldap'))
@Post('login/ldap')
async loginToLDAP(@Req() req: Request): Promise<any> {
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 {}
43 changes: 43 additions & 0 deletions apps/backend/src/authn/authn.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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
) {}

Expand All @@ -26,6 +30,37 @@ export class AuthnService {
}
}

async validateOrCreateUser(
email: string,
firstName: string,
lastName: string
): Promise<any> {
camdenmoors marked this conversation as resolved.
Show resolved Hide resolved
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: ''
};
await this.usersService.create(createUser);
user = await this.usersService.findByEmail(email);
}

if (user) {
this.usersService.updateLoginMetadata(user);
}

return user;
}

async login(user: {
id: string;
email: string;
Expand All @@ -51,4 +86,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]
};
}
}
46 changes: 46 additions & 0 deletions apps/backend/src/authn/ldap.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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
);
camdenmoors marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
84 changes: 84 additions & 0 deletions apps/frontend/src/components/global/login/LDAPLogin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<v-card-text>
<v-form id="login_form" ref="form" name="login_form">
<v-text-field
id="username_field"
v-model="username"
:error-messages="requiredFieldError($v.username, 'Username')"
name="username"
label="Username"
prepend-icon="mdi-account"
type="text"
required
@keyup.enter="$refs.password.focus"
@blur="$v.username.$touch()"
/>
<v-text-field
id="password_field"
ref="password"
v-model="password"
:error-messages="requiredFieldError($v.password, 'Password')"
type="password"
name="password"
label="Password"
prepend-icon="mdi-lock"
@keyup.enter="ldapLogin()"
@blur="$v.password.$touch()"
/>
<v-btn
id="login_button"
depressed
large
color="primary"
@click="ldapLogin()"
>
Login
</v-btn>
</v-form>
</v-card-text>
</template>

<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import {required} from 'vuelidate/lib/validators';
import UserValidatorMixin from '@/mixins/UserValidatorMixin';
import {ServerModule} from '@/store/server';
import {SnackbarModule} from '@/store/snackbar';

export interface LDAPLoginHash {
username: string;
password: string;
}

@Component({
mixins: [UserValidatorMixin],
validations: {
username: {
required
},
password: {
required
}
}
})
export default class LDAPLogin extends Vue {
username: string = '';
password: string = '';

ldapLogin() {
const creds: LDAPLoginHash = {
username: this.username,
password: this.password
};
ServerModule.LoginLDAP(creds)
.then(() => {
this.$router.push('/');
SnackbarModule.notify('You have successfully signed in.');
})
.catch((error) => {
SnackbarModule.notify(error.response.data.message);
});
}
}
</script>
98 changes: 98 additions & 0 deletions apps/frontend/src/components/global/login/LocalLogin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<v-card class="elevation-12 rounded-t-0">
<v-card-text>
<v-form id="login_form" ref="form" name="login_form">
<v-text-field
id="email_field"
v-model="email"
:error-messages="emailErrors($v.email)"
name="email"
label="Email"
prepend-icon="mdi-account"
type="text"
required
@keyup.enter="$refs.password.focus"
@blur="$v.email.$touch()"
/>
<v-text-field
id="password_field"
ref="password"
v-model="password"
:error-messages="requiredFieldError($v.password, 'Password')"
type="password"
name="password"
label="Password"
prepend-icon="mdi-lock"
@keyup.enter="login"
@blur="$v.password.$touch()"
/>
<v-btn
id="login_button"
depressed
large
color="primary"
:disabled="$v.$invalid"
@click="login"
>
Login
</v-btn>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<div class="my-2">
<v-btn id="sign_up_button" depressed small @click="signup">
Sign Up
</v-btn>
</div>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import {ServerModule} from '@/store/server';
import {required, email} from 'vuelidate/lib/validators';
import UserValidatorMixin from '@/mixins/UserValidatorMixin';
import {SnackbarModule} from '@/store/snackbar';

interface LoginHash {
email: string;
password: string;
}
@Component({
mixins: [UserValidatorMixin],
validations: {
email: {
required,
email
},
password: {
required
}
}
})
export default class LocalLogin extends Vue {
email: string = '';
password: string = '';

signup() {
this.$router.push('/signup');
}

login() {
let creds: LoginHash = {
email: this.email,
password: this.password
};
ServerModule.Login(creds)
.then(() => {
this.$router.push('/');
SnackbarModule.notify('You have successfully signed in.');
})
.catch((error) => {
SnackbarModule.notify(error.response.data.message);
});
}
}
</script>
Loading