diff --git a/.env.example b/.env.example index 8a7ab6e..e851513 100644 --- a/.env.example +++ b/.env.example @@ -7,14 +7,17 @@ APP_SECRET=REPLACE_WITH_LONG_SECRET JWT_TOKEN_EXPIRES_IN=30d -# Use NTLM for user authentication -NTLM_AUTH=false +# Use NTLM for user authentication, exposes to browser +VITE_NTLM_AUTH=false # LDAP settings for NTLM authentication LDAP_BASEDN= LDAP_DOMAINSUFFIX= LDAP_USERNAME= LDAP_PASSWORD= +# User object attributes for docmost +LDAP_NAMEATTRIBUTE= +LDAP_MAILATTRIBUTE= DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public" REDIS_URL=redis://127.0.0.1:6379 diff --git a/apps/client/src/features/auth/hooks/use-auth.ts b/apps/client/src/features/auth/hooks/use-auth.ts index ede22a0..56b89ea 100644 --- a/apps/client/src/features/auth/hooks/use-auth.ts +++ b/apps/client/src/features/auth/hooks/use-auth.ts @@ -1,5 +1,5 @@ import { useState } from "react"; -import { login, setupWorkspace } from "@/features/auth/services/auth-service"; +import { login, ntlmLogin, setupWorkspace } from "@/features/auth/services/auth-service"; import { useNavigate } from "react-router-dom"; import { useAtom } from "jotai"; import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; @@ -38,6 +38,25 @@ export default function useAuth() { } }; + const handleNtlmSignIn = async () => { + setIsLoading(true); + + try { + const res = await ntlmLogin(); + setIsLoading(false); + setAuthToken(res.tokens); + + navigate(APP_ROUTE.HOME); + } catch (err) { + console.log(err); + setIsLoading(false); + notifications.show({ + message: err.response?.data.message, + color: "red", + }); + } + }; + const handleInvitationSignUp = async (data: IAcceptInvite) => { setIsLoading(true); @@ -107,6 +126,7 @@ export default function useAuth() { return { signIn: handleSignIn, + ntlmSignIn: handleNtlmSignIn, invitationSignup: handleInvitationSignUp, setupWorkspace: handleSetupWorkspace, isAuthenticated: handleIsAuthenticated, diff --git a/apps/client/src/features/auth/services/auth-service.ts b/apps/client/src/features/auth/services/auth-service.ts index b34b47f..6a56679 100644 --- a/apps/client/src/features/auth/services/auth-service.ts +++ b/apps/client/src/features/auth/services/auth-service.ts @@ -6,12 +6,19 @@ import { ISetupWorkspace, ITokenResponse, } from "@/features/auth/types/auth.types"; +import axios from "axios"; export async function login(data: ILogin): Promise { const req = await api.post("/auth/login", data); return req.data; } +export async function ntlmLogin(): Promise { + // Use separate axios instance to avoid passing app auth headers to allow for NTLM authentication challenge + const req = await axios.post("/api/auth/ntlm"); + return req.data; +} + /* export async function register(data: IRegister): Promise { const req = await api.post("/auth/register", data); diff --git a/apps/client/src/pages/auth/login.tsx b/apps/client/src/pages/auth/login.tsx index c3f47be..e08941a 100644 --- a/apps/client/src/pages/auth/login.tsx +++ b/apps/client/src/pages/auth/login.tsx @@ -1,13 +1,28 @@ import { LoginForm } from "@/features/auth/components/login-form"; +import useAuth from "@/features/auth/hooks/use-auth"; +import { useEffect } from "react"; import { Helmet } from "react-helmet-async"; + +const ntlmAuth = import.meta.env.VITE_NTLM_AUTH; + export default function LoginPage() { + + const { ntlmSignIn } = useAuth(); + + useEffect(() => { + + if (ntlmAuth) + ntlmSignIn(); + + }, []) + return ( <> Login - + {!ntlmAuth && } ); } diff --git a/apps/server/src/core/auth/auth.module.ts b/apps/server/src/core/auth/auth.module.ts index eeb68f5..44f66e4 100644 --- a/apps/server/src/core/auth/auth.module.ts +++ b/apps/server/src/core/auth/auth.module.ts @@ -10,5 +10,6 @@ import { TokenModule } from './token.module'; imports: [TokenModule, WorkspaceModule], controllers: [AuthController], providers: [AuthService, SignupService, JwtStrategy], + exports: [AuthService] }) export class AuthModule {} diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 6c535d1..57c1e90 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -126,6 +126,21 @@ export class EnvironmentService { return this.configService.get('LDAP_PASSWORD') } + getLdapNameAttribute(): string { + return this.configService.get('LDAP_NAMEATTRIBUTE') + } + + getLdapMailAttribute(): string { + return this.configService.get('LDAP_MAILATTRIBUTE') + } + + usingNtlmAuth(): boolean { + const ntlmAuth = this.configService + .get('VITE_NTLM_AUTH', 'false') + .toLowerCase(); + return ntlmAuth === 'true'; + } + isCloud(): boolean { const cloudConfig = this.configService .get('CLOUD', 'false') diff --git a/apps/server/src/integrations/ntlm/ntlm.controller.ts b/apps/server/src/integrations/ntlm/ntlm.controller.ts index 9fe0a66..e36f507 100644 --- a/apps/server/src/integrations/ntlm/ntlm.controller.ts +++ b/apps/server/src/integrations/ntlm/ntlm.controller.ts @@ -5,6 +5,7 @@ import { Res, HttpException, HttpStatus, + Post, } from '@nestjs/common'; import { FastifyRequest, FastifyReply } from 'fastify'; @@ -21,19 +22,18 @@ import { NTLMService } from './ntlm.service'; @Controller() export class NTLMController { - constructor(private readonly ntlmService: NTLMService) {} + constructor( + private readonly ntlmService: NTLMService, + private readonly environmentService: EnvironmentService, + ) {} - @Get('auth/ntlm') - async ntlmAuth( - @Req() req: FastifyRequest, - @Res() res: FastifyReply, - ): Promise { + @Post('auth/ntlm') + async ntlmAuth(@Req() req, @Res() res: FastifyReply) { const authHeader = req.headers['authorization']; if (!authHeader) { // Step 1: Challenge the client for NTLM authentication - res.status(401).header('WWW-Authenticate', 'NTLM').send(); - return; + return res.status(401).header('WWW-Authenticate', 'NTLM').send(); } if (authHeader.startsWith('NTLM ')) { @@ -45,11 +45,10 @@ export class NTLMController { const serverChallenge = new NTLMChallengeMessage(clientNegotiation); const base64Challenge = serverChallenge.toBuffer().toString('base64'); - res + return 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); @@ -68,13 +67,17 @@ export class NTLMController { filter: `(userPrincipalName=${clientAuthentication.userName}@${clientAuthentication.domainName}*)`, }); - // Assuming authentication is successful - res.status(200).send(results); - return; + if (results.length == 1) { + const ntlmSignInResult = await this.ntlmService.login( + results.at(0)[this.environmentService.getLdapNameAttribute()], + results.at(0)[this.environmentService.getLdapMailAttribute()], + req.raw.workspaceId, + ); + return res.status(200).send(ntlmSignInResult); + } else return res.status(403).send(); } else { console.warn('Invalid NTLM Message received.'); - res.status(400).send('Invalid NTLM Message'); - return; + return res.status(400).send('Invalid NTLM Message'); } } diff --git a/apps/server/src/integrations/ntlm/ntlm.module.ts b/apps/server/src/integrations/ntlm/ntlm.module.ts index 169b576..2b4c761 100644 --- a/apps/server/src/integrations/ntlm/ntlm.module.ts +++ b/apps/server/src/integrations/ntlm/ntlm.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { NTLMController } from './ntlm.controller'; import { NTLMService } from './ntlm.service'; +import { TokenModule } from 'src/core/auth/token.module'; +import { WorkspaceModule } from 'src/core/workspace/workspace.module'; +import { AuthModule } from 'src/core/auth/auth.module'; @Module({ - controllers: [NTLMController], - providers: [NTLMService] - }) - export class NTLMModule {} + imports: [TokenModule, WorkspaceModule, AuthModule], + controllers: [NTLMController], + providers: [NTLMService], +}) +export class NTLMModule {} diff --git a/apps/server/src/integrations/ntlm/ntlm.service.ts b/apps/server/src/integrations/ntlm/ntlm.service.ts index 8175d25..e53d612 100644 --- a/apps/server/src/integrations/ntlm/ntlm.service.ts +++ b/apps/server/src/integrations/ntlm/ntlm.service.ts @@ -1,10 +1,19 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { EnvironmentService } from '../environment/environment.service'; import * as ldap from 'ldapjs'; +import { UserRepo } from '@docmost/db/repos/user/user.repo'; +import { TokensDto } from 'src/core/auth/dto/tokens.dto'; +import { TokenService } from 'src/core/auth/services/token.service'; +import { AuthService } from 'src/core/auth/services/auth.service'; @Injectable() export class NTLMService { - constructor(private readonly environmentService: EnvironmentService) {} + constructor( + private readonly environmentService: EnvironmentService, + private authService: AuthService, + private tokenService: TokenService, + private userRepo: UserRepo, + ) {} createClient = (domain: string) => ldap.createClient({ @@ -62,4 +71,40 @@ export class NTLMService { }); }); }; + + async login(name: string, email: string, workspaceId: string) { + const user = await this.userRepo.findByEmail(email, workspaceId, false); + + if (!user) { + const tokensR = await this.authService.register( + { + name, + email, + password: this.generateRandomPassword(12), + }, + workspaceId, + ); + + return tokensR; + } + + user.lastLoginAt = new Date(); + await this.userRepo.updateLastLogin(user.id, workspaceId); + + const tokens: TokensDto = await this.tokenService.generateTokens(user); + return { tokens }; + } + + generateRandomPassword(length: number): string { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?'; + let password = ''; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + password += characters[randomIndex]; + } + + return password; + } }