added MS OAuth support
This commit is contained in:
parent
69c3059ef0
commit
87bac432c8
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/server/node_modules/
|
||||
/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",
|
||||
"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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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'
|
||||
|
||||
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",
|
||||
"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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}`);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user