Skip to content

Commit 8e1be77

Browse files
committed
feat: add rudimentary implementation for *alternate OTPs* (#196)
What did I create... I don't have much time for Mc-Auth right now ,_,
1 parent 8010e54 commit 8e1be77

File tree

4 files changed

+66
-0
lines changed

4 files changed

+66
-0
lines changed

database-setup.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ CREATE TABLE "public"."otps" (
180180
);
181181
COMMENT ON TABLE "public"."otps" IS 'OTPs or One-Time-Passwords';
182182

183+
-- ----------------------------
184+
-- Table structure for alternate_otps
185+
-- ----------------------------
186+
DROP TABLE IF EXISTS "public"."alternate_otps";
187+
CREATE TABLE "public"."alternate_otps" (
188+
"account" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
189+
"code_prefix" varchar(1) COLLATE "pg_catalog"."default" NOT NULL,
190+
"code" int4 NOT NULL,
191+
"issued" timestamptz(0) DEFAULT CURRENT_TIMESTAMP
192+
);
193+
183194
-- ----------------------------
184195
-- Table structure for sessions
185196
-- ----------------------------
@@ -247,6 +258,11 @@ ALTER TABLE "public"."icons" ADD CONSTRAINT "images_pkey" PRIMARY KEY ("id");
247258
-- ----------------------------
248259
ALTER TABLE "public"."otps" ADD CONSTRAINT "otps_pkey" PRIMARY KEY ("account");
249260

261+
-- ----------------------------
262+
-- Primary Key structure for table alternate_otps
263+
-- ----------------------------
264+
ALTER TABLE "public"."alternate_otps" ADD CONSTRAINT "alternate_otps_pkey" PRIMARY KEY ("account");
265+
250266
-- ----------------------------
251267
-- Indexes structure for table sessions
252268
-- ----------------------------

src/Router/OAuthRouter.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,43 @@ export default class OAuthRouter {
276276
});
277277
});
278278

279+
router.all('/alternate-code-exchange', (req, res, next) => {
280+
const supportedBodyContentTypes = ['application/json', 'application/x-www-form-urlencoded'];
281+
282+
handleRequestRestfully(req, res, next, {
283+
post: () => {
284+
if (!req.is(supportedBodyContentTypes)) {
285+
return next(ApiError.create(ApiErrs.unsupportedBodyContentType(req.header('Content-Type') ?? '', supportedBodyContentTypes)));
286+
}
287+
288+
const clientID = req.body['client_id'],
289+
clientSecret = req.body['client_secret'],
290+
mcId = req.body['mc_id'],
291+
code = req.body['code'];
292+
293+
if (!clientID || !clientSecret || !mcId) return next(ApiError.create(ApiErrs.INVALID_CLIENT_ID_OR_SECRET));
294+
if (typeof mcId !== 'string' || mcId.replaceAll('-', '').length != 32) return next(ApiError.create(ApiErrs.INVALID_CODE_FOR_ALTERNATE_TOKEN_EXCHANGE));
295+
if (typeof code !== 'string' || code.replaceAll(' ', '').length != 7 || !StringUtils.isNumeric(code.replaceAll(' ', '').substring(1))) return next(ApiError.create(ApiErrs.INVALID_CODE_FOR_ALTERNATE_TOKEN_EXCHANGE));
296+
297+
db.getApp(clientID)
298+
.then((app) => {
299+
if (!app || app.deleted || app.secret != clientSecret) return next(ApiError.create(ApiErrs.INVALID_CLIENT_ID_OR_SECRET));
300+
301+
return db.invalidateAlternateOneTimePassword(mcId.replaceAll('-', ''), code.replaceAll(' ', '').charAt(0), code.replaceAll(' ', '').substring(1))
302+
.then(async (success) => {
303+
if (!success) return next(ApiError.create(ApiErrs.INVALID_CODE_FOR_ALTERNATE_TOKEN_EXCHANGE));
304+
305+
return res.send({
306+
_info: 'WARNING: This endpoint is subject to change in the future. This response will contain a "_warning" field (please add logging for it or join my Discord server https://sprax.me/discord for a notification).',
307+
profile: await getMinecraftApi().getProfile(mcId)
308+
});
309+
});
310+
})
311+
.catch(next);
312+
}
313+
});
314+
});
315+
279316
return router;
280317
}
281318
}

src/utils/ApiErrs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class ApiErrs {
1717
static readonly INVALID_REDIRECT_URI_FOR_APP: ApiErrTemplate = { httpCode: 400, message: 'Invalid redirect_uri - Please contact the administrator of the page that sent you here', logErr: false };
1818
static readonly INVALID_GRANT_TYPE: ApiErrTemplate = { httpCode: 400, message: 'Invalid grant_type', logErr: false };
1919
static readonly INVALID_CODE_FOR_TOKEN_EXCHANGE: ApiErrTemplate = { httpCode: 400, message: 'Invalid code! expired? Wrong redirect_uri?', logErr: false };
20+
static readonly INVALID_CODE_FOR_ALTERNATE_TOKEN_EXCHANGE: ApiErrTemplate = { httpCode: 400, message: 'Invalid code! maybe expired?', logErr: false };
2021

2122
static readonly INVALID_OR_EXPIRED_MAIL_CONFIRMATION: ApiErrTemplate = { httpCode: 400, message: 'Invalid or expired email confirmation token', logErr: false };
2223

src/utils/DbUtils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,18 @@ export class DbUtils {
267267
});
268268
}
269269

270+
async invalidateAlternateOneTimePassword(mcUUID: string, otpPrefix: string, otp: string): Promise<boolean> {
271+
return new Promise((resolve, reject) => {
272+
if (this.pool == null) return reject(ApiError.create(ApiErrs.NO_DATABASE, {pool: this.pool}));
273+
274+
this.pool.query(`DELETE FROM alternate_otps WHERE account =$1 AND code_prefix =$2 AND code =$3 AND issued >= CURRENT_TIMESTAMP - INTERVAL '5 MINUTES' RETURNING *;`, [mcUUID, otpPrefix, otp], (err, res) => {
275+
if (err) return reject(err);
276+
277+
resolve(res.rows.length > 0);
278+
});
279+
});
280+
}
281+
270282
/* Grants */
271283

272284
async createGrant(clientID: string, mcUUID: string, redirectURI: string, responseType: string, state: string | null, scopes: string[]): Promise<Grant> {

0 commit comments

Comments
 (0)