Skip to content

Commit

Permalink
feat: initial osTicket SSO implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewL246 committed Dec 21, 2024
1 parent a82e731 commit b8bafa1
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 0 deletions.
94 changes: 94 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@aws-sdk/client-ses": "^3.515.0",
"@discordjs/rest": "^0.5.0",
"@node-oauth/express-oauth-server": "^4.1.1",
"@node-saml/node-saml": "^5.0.0",
"@pretendonetwork/error-codes": "^1.0.3",
"browserify": "^17.0.0",
Expand All @@ -42,6 +43,7 @@
"mii-js": "github:PretendoNetwork/mii-js#v1.0.4",
"mongoose": "^6.4.0",
"morgan": "^1.10.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.7.5",
"stripe": "^9.9.0"
},
Expand Down
141 changes: 141 additions & 0 deletions src/routes/oauth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
const express = require('express');
const OAuthServer = require('@node-oauth/express-oauth-server');
const NodeCache = require('node-cache');

const config = require('../../config.json');
const util = require('../util');

// * osTicket uses the authorization code grant type and requests a new
// * authorization code and access token for each sign-in
const authorizationCodeLifetime = 2 * 60;
const accessTokenLifetime = 2 * 60;
const authorizationCodeCache = new NodeCache({
stdTTL: authorizationCodeLifetime,
checkperiod: authorizationCodeLifetime / 2
});
const accessTokenCache = new NodeCache({
stdTTL: accessTokenLifetime,
checkperiod: accessTokenLifetime / 2
});

const router = new express.Router();

async function getClient(clientId, clientSecret) {
console.log(`getClient(clientId: ${JSON.stringify(clientId, null, 2)}, clientSecret: ${JSON.stringify(clientSecret, null, 2)})`);
if (clientId === config.osticket.oauth.client_id &&
(!clientSecret || clientSecret === config.osticket.oauth.client_secret)) {
return {
id: clientId,
redirectUris: [config.osticket.oauth.redirect_uri],
grants: ['authorization_code']
};
} else {
return null;
}
}

async function saveAuthorizationCode(code, client, user) {
console.log(`saveAuthorizationCode(code: ${JSON.stringify(code, null, 2)}, client: ${JSON.stringify(client, null, 2)}, user: ${JSON.stringify(user, null, 2)})`);
const authorizationCodeData = {
authorizationCode: code.authorizationCode,
expiresAt: new Date(Date.now() + authorizationCodeLifetime * 1000),
redirectUri: code.redirectUri,
scope: code.scope,
client: client,
user: user
};

authorizationCodeCache.set(code.authorizationCode, authorizationCodeData);

return authorizationCodeData;
}

async function getAuthorizationCode(authorizationCode) {
console.log(`getAuthorizationCode(authorizationCode: ${JSON.stringify(authorizationCode, null, 2)})`);
const authorizationCodeData = authorizationCodeCache.get(authorizationCode);

if (authorizationCodeData) {
return authorizationCodeData;
} else {
return null;
}
}
async function revokeAuthorizationCode(code) {
console.log(`revokeAuthorizationCode(code: ${JSON.stringify(code, null, 2)})`);
const deletedKeys = authorizationCodeCache.del(code.authorizationCode);

return deletedKeys > 0;
}

async function saveToken(token, client, user) {
console.log(`saveToken(token: ${JSON.stringify(token, null, 2)}, client: ${JSON.stringify(client, null, 2)}, user: ${JSON.stringify(user, null, 2)})`);
// * osTicket doesn't use refresh tokens
const accessTokenData = {
accessToken: token.accessToken,
accessTokenExpiresAt: new Date(Date.now() + accessTokenLifetime * 1000),
scope: token.scope,
client: client,
user: user
};

accessTokenCache.set(token.accessToken, accessTokenData);

return accessTokenData;
}


async function getAccessToken(accessToken) {
console.log(`getAccessToken(accessToken: ${JSON.stringify(accessToken, null, 2)})`);
const accessTokenData = accessTokenCache.get(accessToken);

if (accessTokenData) {
return accessTokenData;
} else {
return null;
}
}

async function handleAuthentication(request, response) {
console.log('handleAuthentication(request, response)');
if (request.cookies.access_token && request.cookies.refresh_token) {
try {
const accountData = await util.getUserAccountData(request, response);

// * Use the same fake email address as Discourse SSO for forwarding
return {
username: accountData.username,
email: `${accountData.pid}@forward.local`, //TODO: Choose a domain
};
} catch (error) {
console.log(error);
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return false;
}
} else {
return false;
}
}

const oauth = new OAuthServer({
model: {
getClient,
saveAuthorizationCode,
getAuthorizationCode,
revokeAuthorizationCode,
saveToken,
getAccessToken
}
});


router.use('/authorize', oauth.authorize({
authenticateHandler: {
handle: handleAuthentication
}
}));
router.use('/token', oauth.token());
router.get('/details', oauth.authenticate(), (request, response) => {
response.json(response.locals.oauth.token.user);
});

module.exports = router;
2 changes: 2 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const routes = {
docs: require('./routes/docs'),
progress: require('./routes/progress'),
account: require('./routes/account'),
oauth: require('./routes/oauth'),
blog: require('./routes/blog'),
localization: require('./routes/localization'),
aprilfools: require('./routes/aprilfools')
Expand All @@ -109,6 +110,7 @@ app.use('/faq', routes.faq);
app.use('/docs', routes.docs);
app.use('/progress', routes.progress);
app.use('/account', routes.account);
app.use('/oauth', routes.oauth);
app.use('/localization', routes.localization);
app.use('/blog', routes.blog);
app.use('/nso-legacy-pack', routes.aprilfools);
Expand Down

0 comments on commit b8bafa1

Please sign in to comment.