mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-17 18:21:04 +10:00
Create Auth/NTLM Endpoint
- Adds NTLM challenge negotiation - Checks NTLM auth user & domain against AD / LDAP and returns info - Adds relevant .env entries
This commit is contained in:
@ -59,11 +59,13 @@
|
||||
"happy-dom": "^15.7.3",
|
||||
"kysely": "^0.27.4",
|
||||
"kysely-migration-cli": "^0.4.2",
|
||||
"ldapjs": "^3.0.7",
|
||||
"marked": "^13.0.3",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"nestjs-kysely": "^1.0.0",
|
||||
"nodemailer": "^6.9.14",
|
||||
"ntlm-server": "^0.1.3",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.12.0",
|
||||
"pg-tsquery": "^8.4.2",
|
||||
@ -85,6 +87,7 @@
|
||||
"@types/debounce": "^1.2.4",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/ldapjs": "^3.0.6",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^22.5.2",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
|
||||
@ -14,6 +14,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { HealthModule } from './integrations/health/health.module';
|
||||
import { ExportModule } from './integrations/export/export.module';
|
||||
import { ImportModule } from './integrations/import/import.module';
|
||||
import { NTLMModule } from './integrations/ntlm/ntlm.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -27,6 +28,7 @@ import { ImportModule } from './integrations/import/import.module';
|
||||
HealthModule,
|
||||
ImportModule,
|
||||
ExportModule,
|
||||
NTLMModule,
|
||||
StorageModule.forRootAsync({
|
||||
imports: [EnvironmentModule],
|
||||
}),
|
||||
|
||||
@ -110,6 +110,22 @@ export class EnvironmentService {
|
||||
return this.configService.get<string>('POSTMARK_TOKEN');
|
||||
}
|
||||
|
||||
getLdapBaseDn(): string {
|
||||
return this.configService.get<string>('LDAP_BASEDN')
|
||||
}
|
||||
|
||||
getLdapDomainSuffix(): string {
|
||||
return this.configService.get<string>('LDAP_DOMAINSUFFIX');
|
||||
}
|
||||
|
||||
getLdapUsername(): string {
|
||||
return this.configService.get<string>('LDAP_USERNAME')
|
||||
}
|
||||
|
||||
getLdapPassword(): string {
|
||||
return this.configService.get<string>('LDAP_PASSWORD')
|
||||
}
|
||||
|
||||
isCloud(): boolean {
|
||||
const cloudConfig = this.configService
|
||||
.get<string>('CLOUD', 'false')
|
||||
|
||||
83
apps/server/src/integrations/ntlm/ntlm.controller.ts
Normal file
83
apps/server/src/integrations/ntlm/ntlm.controller.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Req,
|
||||
Res,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
import {
|
||||
NTLMNegotiationMessage,
|
||||
NTLMChallengeMessage,
|
||||
NTLMAuthenticateMessage,
|
||||
MessageType,
|
||||
} from 'ntlm-server';
|
||||
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import { NTLMService } from './ntlm.service';
|
||||
|
||||
@Controller()
|
||||
export class NTLMController {
|
||||
constructor(private readonly ntlmService: NTLMService) {}
|
||||
|
||||
@Get('auth/ntlm')
|
||||
async ntlmAuth(
|
||||
@Req() req: FastifyRequest,
|
||||
@Res() res: FastifyReply,
|
||||
): Promise<void> {
|
||||
const authHeader = req.headers['authorization'];
|
||||
|
||||
if (!authHeader) {
|
||||
// Step 1: Challenge the client for NTLM authentication
|
||||
res.status(401).header('WWW-Authenticate', 'NTLM').send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (authHeader.startsWith('NTLM ')) {
|
||||
// Step 2: Handle NTLM negotiation message
|
||||
const clientNegotiation = new NTLMNegotiationMessage(authHeader);
|
||||
|
||||
if (clientNegotiation.messageType === MessageType.NEGOTIATE) {
|
||||
// Step 3: Send NTLM challenge message
|
||||
const serverChallenge = new NTLMChallengeMessage(clientNegotiation);
|
||||
const base64Challenge = serverChallenge.toBuffer().toString('base64');
|
||||
|
||||
res
|
||||
.status(401)
|
||||
.header('WWW-Authenticate', `NTLM ${base64Challenge}`)
|
||||
.send();
|
||||
return;
|
||||
} else if (clientNegotiation.messageType === MessageType.AUTHENTICATE) {
|
||||
// Step 4: Handle NTLM Authenticate message
|
||||
const clientAuthentication = new NTLMAuthenticateMessage(authHeader);
|
||||
|
||||
// Here you'd perform LDAP or Active Directory authentication
|
||||
|
||||
const client = this.ntlmService.createClient(
|
||||
clientAuthentication.domainName,
|
||||
);
|
||||
|
||||
// Asynchronous bind to AD
|
||||
await this.ntlmService.bindAsync(client);
|
||||
|
||||
const results = await this.ntlmService.searchAsync(client, {
|
||||
scope: 'sub',
|
||||
filter: `(userPrincipalName=${clientAuthentication.userName}@${clientAuthentication.domainName}*)`,
|
||||
});
|
||||
|
||||
// Assuming authentication is successful
|
||||
res.status(200).send(results);
|
||||
return;
|
||||
} else {
|
||||
console.warn('Invalid NTLM Message received.');
|
||||
res.status(400).send('Invalid NTLM Message');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(400).send('Bad NTLM request');
|
||||
}
|
||||
}
|
||||
9
apps/server/src/integrations/ntlm/ntlm.module.ts
Normal file
9
apps/server/src/integrations/ntlm/ntlm.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NTLMController } from './ntlm.controller';
|
||||
import { NTLMService } from './ntlm.service';
|
||||
|
||||
@Module({
|
||||
controllers: [NTLMController],
|
||||
providers: [NTLMService]
|
||||
})
|
||||
export class NTLMModule {}
|
||||
65
apps/server/src/integrations/ntlm/ntlm.service.ts
Normal file
65
apps/server/src/integrations/ntlm/ntlm.service.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import * as ldap from 'ldapjs';
|
||||
|
||||
@Injectable()
|
||||
export class NTLMService {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
|
||||
createClient = (domain: string) =>
|
||||
ldap.createClient({
|
||||
url: 'ldap://' + domain + this.environmentService.getLdapDomainSuffix(),
|
||||
});
|
||||
|
||||
// Promisified version of ldap.Client.bind
|
||||
bindAsync = (client: ldap.Client): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.bind(
|
||||
this.environmentService.getLdapUsername(),
|
||||
this.environmentService.getLdapPassword(),
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Promisified version of client.search
|
||||
searchAsync = (
|
||||
client: ldap.Client,
|
||||
options: ldap.SearchOptions,
|
||||
): Promise<any[]> => {
|
||||
const baseDN: string = this.environmentService.getLdapBaseDn();
|
||||
return new Promise((resolve, reject) => {
|
||||
const entries: any[] = [];
|
||||
|
||||
client.search(baseDN, options, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
const attributes = Object.fromEntries(
|
||||
entry.attributes.map(({ type, values }) => [
|
||||
type,
|
||||
values.length > 1 ? values : values[0],
|
||||
]),
|
||||
);
|
||||
entries.push(attributes);
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(entries);
|
||||
});
|
||||
|
||||
res.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user