added MS OAuth support

This commit is contained in:
MattLeo 2025-12-02 13:49:51 -06:00
parent 69c3059ef0
commit 87bac432c8
10 changed files with 284 additions and 2 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
/server/node_modules/
/server/ka.db
/client/node_modules/
/client/node_modules/
keys.txt

View File

@ -8,6 +8,7 @@
"name": "kb-frontend",
"version": "0.0.0",
"dependencies": {
"@azure/msal-browser": "^4.26.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-quill": "^2.0.0"
@ -24,6 +25,27 @@
"vite": "^7.2.4"
}
},
"node_modules/@azure/msal-browser": {
"version": "4.26.2",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz",
"integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "15.13.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "15.13.2",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz",
"integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View File

@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@azure/msal-browser": "^4.26.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-quill": "^2.0.0"

View File

@ -104,4 +104,59 @@
.link-button:disabled {
color: #95a5a6;
cursor: not-allowed;
}
.divider {
margin: 1.5rem 0;
text-align: center;
position: relative;
}
.divider::before,
.divider::after {
content: '';
position: absolute;
top: 50%;
width: 40%;
height: 1px;
background-color: #ddd;
}
.divider::before {
left: 0;
}
.divider::after {
right: 0;
}
.divider span {
background-color: white;
padding: 0 1rem;
color: #999;
}
.microsoft-login-button {
width: 100%;
padding: 0.75rem;
background-color: white;
color: #5e5e5e;
border: 1px solid #8c8c8c;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: background-color 0.3s;
}
.microsoft-login-button:hover:not(:disabled) {
background-color: #f5f5f5;
}
.microsoft-login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}

View File

@ -1,4 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { PublicClientApplication } from '@azure/msal-browser';
import { msalConfig, loginRequest } from '../msalConfig';
import './Login.css';
function Login({ onLoginSuccess, onSwitchToRegister }) {
@ -6,6 +8,17 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [msalInstance, setMsalInstance] = useState(null);
useEffect(() => {
const initializeMasal = async () => {
const instance = new PublicClientApplication(msalConfig);
await instance.initialize();
setMsalInstance(instance);
};
initializeMasal();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
@ -39,6 +52,45 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
}
};
const handleMicrosoftLogin = async () => {
if (!msalInstance) {
setError('Microsoft login is initializing, please try again');
return;
}
setLoading(true);
setError('');
try {
// Trigger Microsoft login popup - this gets us the access token directly
const loginResponse = await msalInstance.loginPopup(loginRequest);
// Send the access token to your backend
const response = await fetch('http://localhost:9000/api/auth/microsoft', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
accessToken: loginResponse.accessToken // Send access token instead of code
})
});
const data = await response.json();
if (response.ok) {
onLoginSuccess(data.user, data.token);
} else {
setError(data.error || 'Microsoft login failed');
}
} catch (err) {
console.error('Microsoft login error:', err);
setError('Failed to login with Microsoft');
} finally {
setLoading(false);
}
};
return (
<div className='login-container'>
<div className='login-box'>
@ -75,6 +127,25 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
</button>
</form>
<div className="divider">
<span>OR</span>
</div>
<button
type="button"
className="microsoft-login-button"
onClick={handleMicrosoftLogin}
disabled={loading}
>
<svg width="21" height="21" viewBox="0 0 21 21" fill="none">
<rect x="1" y="1" width="9" height="9" fill="#f25022"/>
<rect x="1" y="11" width="9" height="9" fill="#00a4ef"/>
<rect x="11" y="1" width="9" height="9" fill="#7fba00"/>
<rect x="11" y="11" width="9" height="9" fill="#ffb900"/>
</svg>
Sign in with Microsoft
</button>
<div className='switch-auth'>
<button
type='button'

View File

@ -0,0 +1,15 @@
export const msalConfig = {
auth: {
clientId: '819be79f-6392-4c9d-9b01-2c4ea108f31f',
authority: 'https://login.microsoftonline.com/82bf3877-2abc-4412-aeca-2985f992a32f',
redirectUri: 'http://localhost:5173',
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false
}
};
export const loginRequest = {
scopes: ['User.Read']
};

16
server/entraConfig.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
auth: {
clientId: '819be79f-6392-4c9d-9b01-2c4ea108f31f',
authority: 'https://login.microsoftonline.com/82bf3877-2abc-4412-aeca-2985f992a32f',
clientSecret: 'f5ddb8c1-8239-41a0-822c-807854b32325'
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: 'Info',
}
}
};

View File

@ -9,6 +9,7 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@azure/msal-node": "^3.8.3",
"argon2": "^0.44.0",
"cors": "^2.8.5",
"express": "^5.1.0",
@ -16,6 +17,29 @@
"sql.js": "^1.13.0"
}
},
"node_modules/@azure/msal-common": {
"version": "15.13.2",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz",
"integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.3.tgz",
"integrity": "sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "15.13.2",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
@ -1077,6 +1101,15 @@
"node": ">= 0.8"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -18,6 +18,7 @@
"author": "Matt Taylor",
"license": "MIT",
"dependencies": {
"@azure/msal-node": "^3.8.3",
"argon2": "^0.44.0",
"cors": "^2.8.5",
"express": "^5.1.0",

View File

@ -15,11 +15,15 @@ const {
const { generateToken, authenticateToken } = require('./auth');
const app = express();
const cors = require('cors');
const msal = require('@azure/msal-node');
const entraConfig = require('./entraConfig');
const PORT = 9000;
app.use(cors())
app.use(express.json());
const msalClient = new msal.ConfidentialClientApplication(entraConfig);
initDb().then(() => {
app.get('/', (req, res) => {
@ -194,6 +198,69 @@ initDb().then(() => {
}
});
app.post('/api/auth/microsoft', async (req, res) => {
try {
const { accessToken } = req.body;
if (!accessToken) {
return res.status(400).json({error: 'Access token required'});
}
// Get user info directly from Microsoft Graph using the access token
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!graphResponse.ok) {
return res.status(401).json({error: 'Invalid Microsoft token'});
}
const msUser = await graphResponse.json();
// Check if user exists in our database
let user = getUserByEmail(msUser.mail || msUser.userPrincipalName);
if (!user) {
// JIT Provisioning - Create new user
user = createUser(
null,
msUser.mail || msUser.userPrincipalName,
null,
msUser.displayName,
'entra',
msUser.id
);
} else if (user.auth_provider === 'local') {
return res.status(400).json({
error: 'This email is registered with a local account. Please login with username/password.'
});
}
// Generate our JWT token
const token = generateToken(user);
return res.status(200).json({
user: {
id: user.id,
username: user.username,
email: user.email,
display_name: user.display_name,
auth_provider: user.auth_provider,
created_at: user.created_at
},
token
});
} catch(err) {
console.error('Microsoft auth error:', err);
return res.status(500).json({
error: 'Microsoft authentication failed',
details: String(err)
});
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});