Compare commits

...

5 commits

Author SHA1 Message Date
Angelo b184581281
Merge e3e8c6f4aa into 79d22e2ec8 2024-05-11 12:17:35 -04:00
Jonathan Barrow 79d22e2ec8
Merge pull request #295 from PretendoNetwork/discourse-sso 2024-05-09 11:40:22 -04:00
Jonathan Barrow 09b53b7814
fix: add forum link to header 2024-05-09 11:37:53 -04:00
Jonathan Barrow 255e68a881
feat: discourse SSO 2024-05-07 17:19:03 -04:00
Christopher Angelo e3e8c6f4aa
docs: improve 3DS installation preamble
* Inlined multiple tips and caution to be a dedication section "Things to know before you start" section
* Introduced an opening statement on how to connect to pretendo and what the guide will achieve
* Extended the guide steps to add the "Signing into your PNID" section
2024-04-24 12:03:22 +07:00
7 changed files with 177 additions and 19 deletions

View file

@ -1,23 +1,24 @@
# 3DS/2DS Family
<div class="tip red">
<strong>CAUTION:</strong>
SYSTEM TRANSFERS ARE NOT CURRENTLY SUPPORTED BY OUR SERVERS. ATTEMPTING TO PERFORM A SYSTEM TRANSFER MAY PREVENT YOU FROM BEING ABLE TO GO ONLINE IN THE FUTURE. SUPPORT FOR SYSTEM TRANSFERS IS IN DEVELOPMENT.
</div>
## Things to know before you start
<div class="tip red">
<strong>CAUTION:</strong>
Collecting badges in Nintendo Badge Arcade while connected to one network and then launching the game on a different network will result in your badges disappearing. This occurs because the locally saved data does not match the data stored on the server.
</div>
This guide assumes that you have a **Homebrewed System** running the latest version of Luma3DS (13+). If you are unsure or do not have a homebrewed system, please follow the [3DS Hacks Guide](https://3ds.hacks.guide/) to check, install and update it.
<div class="tip">
This guide assumes that you have a <b>Homebrewed System running the latest version of Luma3DS (13+)</b>, if you don't please follow this <a href="https://3ds.hacks.guide/" target="_blank">guide</a> on how to homebrew your system first.
</div>
**Support for system transfer is currently in development**. Attempting to perform one while being connected to Pretendo will prevent you from being able to go online in the future. If you need to do a system transfer, please do it before continuing with this guide.
The following steps are required for you to connect to the Pretendo Network:
Badges in **Nintendo Badge Arcade** might disappear and become de-synchronized when switching between Pretendo and Nintendo Network. This is due to your local data not matching the data stored on the respective server.
## Getting Started
To connect to the Pretendo Network, you will need to install a homebrew application and enable Luma patches on your console.
Nimbus is our homebrew application that allows you to connect and switch from Nintendo Network to the Pretendo Network.
This guide is divided into the following steps:
1. [Downloading Nimbus](#downloading-nimbus)
2. [Enabling Luma patches](#luma-patches)
3. [Nimbus](#using-nimbus)
3. [Using Nimbus](#using-nimbus)
4. [Signing into your PNID](#signing-into-your-pnid)
## Downloading Nimbus

View file

@ -92,6 +92,11 @@ async function renderDataMiddleware(request, response, next) {
request.pnid = await database.PNID.findOne({ pid: response.locals.account.pid });
request.account = response.locals.account;
if (request.pnid.deleted) {
// TODO - We just need to overhaul our API tbh
throw new Error('User not found');
}
return next();
} catch (error) {
response.cookie('error_message', error.message, { domain: '.pretendo.network' });

View file

@ -71,7 +71,8 @@ router.get('/', requireLoginMiddleware, async (request, response) => {
router.get('/login', async (request, response) => {
const renderData = {
error: request.cookies.error_message
error: request.cookies.error_message,
loginPath: '/account/login'
};
response.render('account/login', renderData);
@ -88,7 +89,6 @@ router.post('/login', async (request, response) => {
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
response.redirect(request.redirect || '/account');
} catch (error) {
console.log(error);
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
@ -407,5 +407,138 @@ router.post('/stripe/webhook', async (request, response) => {
response.json({ received: true });
});
router.get('/sso/discourse', async (request, response, next) => {
if (!request.query.sso || !request.query.sig) {
return next(); // * 404
}
const signature = util.signDiscoursePayload(request.query.sso);
if (signature !== request.query.sig) {
return next(); // * 404
}
const decodedPayload = new URLSearchParams(Buffer.from(request.query.sso, 'base64').toString());
if (!decodedPayload.has('nonce') || !decodedPayload.has('return_sso_url')) {
return next(); // * 404
}
// * User already logged in, don't show the login prompt
if (request.cookies.access_token && request.cookies.refresh_token) {
try {
const accountData = await util.getUserAccountData(request, response);
// * Discourse REQUIRES unique emails, however we do not due to NN also
// * not requiring unique email addresses. Email addresses, for now,
// * are faked using the users PID. This will essentially disable email
// * for the forum, but it's a bullet we have to bite for right now.
// TODO - We can run our own SMTP server which maps fake emails (pid@pretendo.whatever) to users real emails
const payload = Buffer.from(new URLSearchParams({
nonce: decodedPayload.get('nonce'),
external_id: accountData.pid,
email: `${accountData.pid}@invalid.com`, // * Hack to get unique emails
username: accountData.username,
name: accountData.username,
avatar_url: accountData.mii.image_url,
avatar_force_update: true
}).toString()).toString('base64');
const query = new URLSearchParams({
sso: payload,
sig: util.signDiscoursePayload(payload)
}).toString();
return response.redirect(`${decodedPayload.get('return_sso_url')}?${query}`);
} catch (error) {
console.log(error);
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account/logout');
}
}
// * User not logged in already, show the login page
const renderData = {
discourse: {
// * Fast and dirty sanitization. If the strings contain
// * characters not allow in their encodings, they are removed
// * when doing this decode-encode. Since neither base64/hex
// * allow characters such as < and >, this prevents injection.
payload: Buffer.from(request.query.sso, 'base64').toString('base64'),
signature: Buffer.from(request.query.sig, 'hex').toString('hex')
},
loginPath: '/account/sso/discourse'
};
response.render('account/login', renderData); // * Just reuse the /account/login page, no need to duplicate the pages
});
router.post('/sso/discourse', async (request, response, next) => {
if (!request.body['discourse-sso-payload'] || !request.body['discourse-sso-signature']) {
return next(); // * 404
}
const { username, password } = request.body;
// * Fast and dirty sanitization. If the strings contain
// * characters not allow in their encodings, they are removed
// * when doing this decode-encode. Since neither base64/hex
// * allow characters such as < and >, this prevents injection.
const discoursePayload = Buffer.from(request.body['discourse-sso-payload'], 'base64').toString('base64');
const discourseSignature = Buffer.from(request.body['discourse-sso-signature'], 'hex').toString('hex');
const signature = util.signDiscoursePayload(discoursePayload);
if (signature !== discourseSignature) {
return next(); // * 404
}
const decodedPayload = new URLSearchParams(Buffer.from(discoursePayload, 'base64').toString());
if (!decodedPayload.has('nonce') || !decodedPayload.has('return_sso_url')) {
return next(); // * 404
}
try {
const tokens = await util.login(username, password);
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
// * Need to set these here so that getUserAccountData can see them
request.cookies.refresh_token = tokens.refresh_token;
request.cookies.access_token = tokens.access_token;
request.cookies.token_type = tokens.token_type;
const accountData = await util.getUserAccountData(request, response);
// * Discourse REQUIRES unique emails, however we do not due to NN also
// * not requiring unique email addresses. Email addresses, for now,
// * are faked using the users PID. This will essentially disable email
// * for the forum, but it's a bullet we have to bite for right now.
// TODO - We can run our own SMTP server which maps fake emails (pid@pretendo.whatever) to users real emails
const payload = Buffer.from(new URLSearchParams({
nonce: decodedPayload.get('nonce'),
external_id: accountData.pid,
email: `${accountData.pid}@invalid.com`, // * Hack to get unique emails
username: accountData.username,
name: accountData.username,
avatar_url: accountData.mii.image_url,
avatar_force_update: true
}).toString()).toString('base64');
const query = new URLSearchParams({
sso: payload,
sig: util.signDiscoursePayload(payload)
}).toString();
return response.redirect(`${decodedPayload.get('return_sso_url')}?${query}`);
} catch (error) {
console.log(error);
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account/login');
}
});
module.exports = router;

View file

@ -2,6 +2,7 @@ const { Schema } = require('mongoose');
// Only define what we will be using
const PNIDSchema = new Schema({
deleted: Boolean,
pid: {
type: Number,
unique: true

View file

@ -247,6 +247,10 @@ async function removeDiscordMemberTesterRole(memberId) {
}
}
function signDiscoursePayload(payload) {
return crypto.createHmac('sha256', config.discourse.sso.secret).update(payload).digest('hex');
}
module.exports = {
fullUrl,
getLocale,
@ -265,5 +269,6 @@ module.exports = {
assignDiscordMemberSupporterRole,
assignDiscordMemberTesterRole,
removeDiscordMemberSupporterRole,
removeDiscordMemberTesterRole
removeDiscordMemberTesterRole,
signDiscoursePayload
};

View file

@ -5,9 +5,8 @@
{{> header}}
<div class="wrapper">
<div class="account-form-wrapper">
<form action="/account/login" method="post" class="account">
<form action="{{ loginPath }}" method="post" class="account">
<h2>{{ locale.account.loginForm.login }}</h2>
<p>{{ locale.account.loginForm.detailsPrompt }}</p>
<div>
@ -23,8 +22,10 @@
<input name="redirect" id="redirect" type="hidden" value="{{redirect}}">
<div class="buttons">
<button type="submit">{{ locale.account.loginForm.login }}</button>
<a href="/account/register{{#if redirect}}?redirect={{redirect}}{{/if}}" class="register">{{ locale.account.loginForm.registerPrompt }}</a>
<a href="/account/register{{#if redirect}}?redirect={{redirect}}{{/if}}" class="register">{{ locale.account.loginForm.registerPrompt }}</a>
</div>
<input type="hidden" id="discourse-sso-payload" name="discourse-sso-payload" value="{{ discourse.payload }}" />
<input type="hidden" id="discourse-sso-signature" name="discourse-sso-signature" value="{{ discourse.signature }}" />
</form>
</div>
</div>

View file

@ -39,6 +39,9 @@
<a href="/progress" class="hide-on-mobile">
<button>{{ locale.nav.progress }}</button>
</a>
<a href="https://forum.pretendo.network" class="hide-on-mobile">
<button>Forum</button> <!-- TODO - Translate this -->
</a>
<a href="/account/upgrade" class="donate">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
@ -161,6 +164,15 @@
<p class="caption">{{ locale.nav.dropdown.captions.blog }}</p>
</div>
</a>
<a href="https://forum.pretendo.network">
<div class="icon">
<svg width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M216 48H56a16 16 0 0 0-16 16v120a8 8 0 0 1-16 0V88a8 8 0 0 0-16 0v96a24 24 0 0 0 24 24h176a24.1 24.1 0 0 0 24-24V64a16 16 0 0 0-16-16Zm-40 104H96a8 8 0 0 1 0-16h80a8 8 0 0 1 0 16Zm0-32H96a8 8 0 0 1 0-16h80a8 8 0 0 1 0 16Z"/></svg>
</div>
<div>
<p class="title">Forum</p> <!-- TODO - Translate this -->
<p class="caption">Chat with others and get support</p> <!-- TODO - Translate this -->
</div>
</a>
<a href="/progress">
<div class="icon">
<svg width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M96 8a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16h-48a8 8 0 0 1-8-8Zm128 120a96 96 0 1 1-96-96a96.2 96.2 0 0 1 96 96Zm-50.7-45.3a8.1 8.1 0 0 0-11.4 0l-39.6 39.6a8.1 8.1 0 0 0 0 11.4a8.2 8.2 0 0 0 11.4 0l39.6-39.6a8.1 8.1 0 0 0 0-11.4Z"/></svg>