From d8782c6b1f2e19100edba2dd5d26bc8e3f8a99ef Mon Sep 17 00:00:00 2001 From: "Qumolama.d" Date: Wed, 4 May 2022 16:48:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20authserver=20=E5=89=A9?= =?UTF-8?q?=E4=BD=99=20API=20=E7=9A=84=E5=AE=9E=E7=8E=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 4 + src/models/token.js | 2 +- src/routes/authenticate.js | 326 ++++++++++++++++++++++++++++++++++++- 3 files changed, 329 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index a7ea905..87eb2c1 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,10 @@ export const server = fastify({ config.custom.preRouting(server) server.route(AuthenticateRoutings.authenticate) + server.route(AuthenticateRoutings.refresh) + server.route(AuthenticateRoutings.validate) + server.route(AuthenticateRoutings.invalidate) + server.route(AuthenticateRoutings.signout) config.custom.postRouting(server) /* diff --git a/src/models/token.js b/src/models/token.js index c7b9481..0e5e634 100644 --- a/src/models/token.js +++ b/src/models/token.js @@ -4,7 +4,7 @@ const { Schema } = mongoose export const TokenSchema = new Schema({ uuid: String, token: String, + clientToken: String, expireDate: Number, deadDate: Number, - state: String, // alive, linbo, dead }) \ No newline at end of file diff --git a/src/routes/authenticate.js b/src/routes/authenticate.js index 1dfa398..a48fc9f 100644 --- a/src/routes/authenticate.js +++ b/src/routes/authenticate.js @@ -55,10 +55,19 @@ export const authenticate = { } }, preHandler: async function(req, rep) { - this.conf.custom.overridePrehandler('/authserver/authenticate') + this.conf.custom.overridePrehandler('/authserver/authenticate', req, rep) }, handler: async function (req, rep) { let { username, password, clientToken, requestUser, agent } = req.body + + if(!username || !password || !agent || agent.name.toLowerCase() !== 'minecraft') { + rep.code(418).send({ + error: "ForbiddenOperationException", + errorMessage: "无效应用名,此服务端仅支援 Minecraft", + cause: "此服务器只支持 agent.name: minecraft" + }) + } + const player = await this.models.Player.findOne({ email: username, password: createHash('sha256').update(password).digest().toString('hex').toLowerCase() }) if(!player || !player.permissions.some((it) => { return it.node === 'login' && it.allowed && (it.duration === 0 || it.startDate + it.duration > Date.now()) @@ -100,6 +109,13 @@ export const authenticate = { } } + if(player.textures.skin && player.textures.skin != 0) { // Must be '!=' if this change to '!==' will never works + textures.textures.CAPE = { + url: player.textures.cape, + metadata + } + } + const profile = { uuid: uuidToNoSymboUUID(player.uuid), name: player.username, @@ -114,9 +130,9 @@ export const authenticate = { new this.models.Token({ uuid: player.uuid, token: token, + clientToken: clientToken, expireDate: Date.now() + 1000 * 60 * 60 * 24 * 15, deadDate: Date.now() + 1000 * 60 * 60 * 24 * 30, - state: 'alive' }).save() return await rep.send({ @@ -126,5 +142,311 @@ export const authenticate = { selectedProfile: profile, user: account }) + } +} + +export const refresh = { + method: 'POST', + url: '/authserver/refresh', + schema: { + body: { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "clientToken": { + "type": "string", + "optional": true + }, + "requestUser": { + "type": "boolean", + "optional": true + }, + "selectedProfile": { + "optional": true, + ...PlayerModel.PlayerSeriliazationSchema + } + } + }, + response: { + 200: { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "clientToken": { + "type": "string" + }, + "selectedProfile": PlayerModel.PlayerSeriliazationSchema, + "user": PlayerModel.PlayerAccountSerializationSchema + } + } + } + }, + preHandler: async function(req, rep) { + this.conf.custom.overridePrehandler('/authserver/refresh', req, rep) + }, + handler: async function (req, rep) { + const { accessToken, clientToken, requestUser, selectedProfile } = req.body + + const query = { + token: accessToken + } + + if(clientToken) { + query.clientToken = clientToken + } + const token = await this.models.Token.findOne(query) + + if(!token) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "accessToken无效", + cause: "accessToken无效" + }) + } + + const { deadDate, uuid } = token + + if(deadDate < Date.now()) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "accessToken无效", + cause: "accessToken无效" + }) + } + + const [newToken, key] = generateToken(token.uuid) + this.log.info(`/authserver/authenticate > 为玩家 ${token.uuid} 刷新令牌: ${token.uuid} 为 ${newToken} | 随机 key = ${key}`) + + await this.models.Token.updateOne({ + token: accessToken, + clientToken: clientToken ?? undefined + }, { + $set: { + expireDate: 0, + deadDate: 0 + } + }) + + new this.models.Token({ + uuid: uuid, + token: newToken, + clientToken: clientToken ?? token.clientToken, + expireDate: Date.now() + 1000 * 60 * 60 * 24 * 15, + deadDate: Date.now() + 1000 * 60 * 60 * 24 * 30, + }).save() + + const response = { + accessToken: newToken, + clientToken: clientToken ?? token.clientToken, + } + + if(requestUser || selectedProfile) { + const player = await this.models.Player.findOne({ uuid }) + + if(requestUser) { + response.user = { + id: uuidToNoSymboUUID(player.uuid), + properties: [ + { + name: "preferredLanguage", + value: "zh_CN" + } + ] + } + } + + if(selectedProfile) { + const textures = { + timestamp: 0, + profileId: uuidToNoSymboUUID(player.uuid), + profileName: player.username, + textures: { } + } + + if(player.textures.skin && player.textures.skin != 0) { // Must be '!=' if this change to '!==' will never works + textures.textures.SKIN = { + url: player.textures.skin, + metadata + } + } + + if(player.textures.skin && player.textures.skin != 0) { // Must be '!=' if this change to '!==' will never works + textures.textures.CAPE = { + url: player.textures.cape, + metadata + } + } + + response.selectedProfile = { + uuid: uuidToNoSymboUUID(uuid), + name: player.username, + properties: [ + { + name: "texturs", + value: Buffer.from(JSON.stringify(textures)).toString('base64') + } + ] + } + } + } + + return await rep.send(response) + } +} + +export const validate = { + method: 'POST', + url: '/authserver/validate', + schema: { + body: { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "clientToken": { + "type": "string", + "optional": true + } + } + }, + response: { + 204: { + "type": "null" + } + } + }, + preHandler: async function(req, rep) { + this.conf.custom.overridePrehandler('/authserver/validate', req, rep) + }, + handler: async function (req, rep) { + const { accessToken, clientToken } = req.body + + const query = { + token: accessToken + } + + if(clientToken) { + query.clientToken = clientToken + } + const token = await this.models.Token.findOne(query) + + if(!token) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "accessToken无效", + cause: "accessToken无效" + }) + } + + const { expireDate } = token + + if(expireDate < Date.now()) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "accessToken无效", + cause: "accessToken无效" + }) + } + + return await rep.code(204).send() + } +} + +export const invalidate = { + method: 'POST', + url: '/authserver/invalidate', + schema: { + body: { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "clientToken": { + "type": "string", + "optional": true + } + } + }, + response: { + 204: { + "type": "null" + } + } + } + , + preHandler: async function(req, rep) { + this.conf.custom.overridePrehandler('/authserver/invalidate', req, rep) + }, + handler: async function (req, rep) { + const { accessToken } = req.body + + const { modifiedCount } = await this.models.Token.updateOne({ + token: accessToken + }, { + $set: { + expireDate: 0, + deadDate: 0 + } + }) + if(modifiedCount === 0) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "accessToken无效", + cause: "accessToken无效" + }) + } + + return await rep.code(204).send() + } +} + +export const signout = { + method: 'POST', + url: '/authserver/signout', + schema: { + body: { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string", + "optional": true + } + } + }, + response: { + 204: { + "type": "null" + } + } + }, + preHandler: async function(req, rep) { + this.conf.custom.overridePrehandler('/authserver/logout', req, rep) + }, + handler: async function (req, rep) { + const { username, password } = req.body + + const player = await this.models.Player.findOne({ email: username, password: createHash('sha256').update(password).digest().toString('hex').toLowerCase() }) + if(!player) { + return await rep.code(401).send({ + error: "Unauthorized", + errorMessage: "用户名或密码错误", + cause: "用户名或密码错误" + }) + } + + await this.models.Token.deleteMany({ + uuid: player.uuid + }) + + rep.code(204).send() } } \ No newline at end of file