added MS OAuth support
This commit is contained in:
parent
69c3059ef0
commit
87bac432c8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/server/node_modules/
|
/server/node_modules/
|
||||||
/server/ka.db
|
/server/ka.db
|
||||||
/client/node_modules/
|
/client/node_modules/
|
||||||
|
keys.txt
|
||||||
22
client/kb-frontend/package-lock.json
generated
22
client/kb-frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "kb-frontend",
|
"name": "kb-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^4.26.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill": "^2.0.0"
|
"react-quill": "^2.0.0"
|
||||||
@ -24,6 +25,27 @@
|
|||||||
"vite": "^7.2.4"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^4.26.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill": "^2.0.0"
|
"react-quill": "^2.0.0"
|
||||||
|
|||||||
@ -105,3 +105,58 @@
|
|||||||
color: #95a5a6;
|
color: #95a5a6;
|
||||||
cursor: not-allowed;
|
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;
|
||||||
|
}
|
||||||
@ -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';
|
import './Login.css';
|
||||||
|
|
||||||
function Login({ onLoginSuccess, onSwitchToRegister }) {
|
function Login({ onLoginSuccess, onSwitchToRegister }) {
|
||||||
@ -6,6 +8,17 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
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 (
|
return (
|
||||||
<div className='login-container'>
|
<div className='login-container'>
|
||||||
<div className='login-box'>
|
<div className='login-box'>
|
||||||
@ -75,6 +127,25 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</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'>
|
<div className='switch-auth'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
|
|||||||
15
client/kb-frontend/src/msalConfig.js
Normal file
15
client/kb-frontend/src/msalConfig.js
Normal 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
16
server/entraConfig.js
Normal 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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
33
server/package-lock.json
generated
33
server/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-node": "^3.8.3",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@ -16,6 +17,29 @@
|
|||||||
"sql.js": "^1.13.0"
|
"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": {
|
"node_modules/@epic-web/invariant": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
@ -1077,6 +1101,15 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
"author": "Matt Taylor",
|
"author": "Matt Taylor",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-node": "^3.8.3",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|||||||
@ -15,11 +15,15 @@ const {
|
|||||||
const { generateToken, authenticateToken } = require('./auth');
|
const { generateToken, authenticateToken } = require('./auth');
|
||||||
const app = express();
|
const app = express();
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const msal = require('@azure/msal-node');
|
||||||
|
const entraConfig = require('./entraConfig');
|
||||||
const PORT = 9000;
|
const PORT = 9000;
|
||||||
|
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
const msalClient = new msal.ConfidentialClientApplication(entraConfig);
|
||||||
|
|
||||||
initDb().then(() => {
|
initDb().then(() => {
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
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, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user