mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-28 01:26:44 -06:00
fixed Oauth2 provider
This commit is contained in:
parent
03d734aac7
commit
df0f5505e5
1 changed files with 260 additions and 281 deletions
|
@ -1,8 +1,3 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import querystring from 'querystring';
|
||||
import dns from 'node:dns/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
@ -66,29 +61,23 @@ const kinds = [
|
|||
function validateClientId(raw: string): URL {
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
|
||||
const allowedProtocols = process.env.NODE_ENV === 'test' ? ['http:', 'https:'] : ['https:'];
|
||||
if (!allowedProtocols.includes(url.protocol)) {
|
||||
throw new AuthorizationError('client_id must be a valid HTTPS URL', 'invalid_request');
|
||||
}
|
||||
|
||||
const segments = url.pathname.split('/');
|
||||
if (segments.includes('.') || segments.includes('..')) {
|
||||
throw new AuthorizationError('client_id must not contain dot path segments', 'invalid_request');
|
||||
}
|
||||
|
||||
if (url.hash) {
|
||||
throw new AuthorizationError('client_id must not contain a fragment component', 'invalid_request');
|
||||
}
|
||||
|
||||
if (url.username || url.password) {
|
||||
throw new AuthorizationError('client_id must not contain a username or a password', 'invalid_request');
|
||||
}
|
||||
|
||||
if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
|
||||
throw new AuthorizationError('client_id must have a domain name as a host name', 'invalid_request');
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new AuthorizationError('client_id must be a valid URL', 'invalid_request');
|
||||
|
@ -130,19 +119,18 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
|
|||
}
|
||||
}
|
||||
|
||||
function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
|
||||
function getClient(BASE_URL: string, authorization: string | undefined, type: string = 'misskey'): MegalodonInterface {
|
||||
const accessTokenArr = authorization?.split(' ') ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
return generator(type, BASE_URL, accessToken) as MegalodonInterface;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2ProviderService {
|
||||
#logger: Logger;
|
||||
#server = oauth2orize.createServer();
|
||||
#clientCache = new MemoryKVCache<ClientInformation>(60 * 60 * 1000); // 1 hour
|
||||
#clientCache = new MemoryKVCache<ClientInformation>(60 * 60 * 1000);
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
|
@ -154,8 +142,6 @@ export class OAuth2ProviderService {
|
|||
this.#server.grant(oauth2Pkce.extensions());
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc8414.html
|
||||
// https://indieauth.spec.indieweb.org/#indieauth-server-metadata
|
||||
public generateRFC8414() {
|
||||
return {
|
||||
issuer: this.config.url,
|
||||
|
@ -181,7 +167,6 @@ export class OAuth2ProviderService {
|
|||
});
|
||||
|
||||
fastify.register(fastifyCors);
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
root: fileURLToPath(new URL('../web/views', import.meta.url)),
|
||||
engine: { pug },
|
||||
|
@ -214,7 +199,6 @@ export class OAuth2ProviderService {
|
|||
|
||||
fastify.register(multer.contentParser);
|
||||
|
||||
// Handle both Misskey and Mastodon OAuth flows
|
||||
fastify.get('/authorize', async (request, reply) => {
|
||||
const query: any = request.query;
|
||||
let param = "mastodon=true";
|
||||
|
@ -222,7 +206,6 @@ export class OAuth2ProviderService {
|
|||
if (query.redirect_uri) param += `&redirect_uri=${query.redirect_uri}`;
|
||||
const client = query.client_id ? query.client_id : "";
|
||||
|
||||
// Misskey OAuth validation
|
||||
if (client) {
|
||||
try {
|
||||
const clientUrl = validateClientId(Buffer.from(client.toString(), 'base64').toString());
|
||||
|
@ -232,14 +215,10 @@ export class OAuth2ProviderService {
|
|||
throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
|
||||
}
|
||||
}
|
||||
|
||||
// Cache client information
|
||||
const clientInfo = await this.#clientCache.fetch(
|
||||
clientUrl.href,
|
||||
() => discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href),
|
||||
);
|
||||
|
||||
// Store client info for token endpoint
|
||||
await this.#clientCache.set(`client:${query.code_challenge}`, clientInfo);
|
||||
} catch (err) {
|
||||
reply.code(400).send({ error: err.message });
|
||||
|
@ -247,9 +226,7 @@ export class OAuth2ProviderService {
|
|||
}
|
||||
}
|
||||
|
||||
reply.redirect(
|
||||
`${Buffer.from(client.toString(), 'base64').toString()}?${param}`,
|
||||
);
|
||||
reply.redirect(`${Buffer.from(client.toString(), 'base64').toString()}?${param}`);
|
||||
});
|
||||
|
||||
fastify.get('/authorize/', (request, reply) => {
|
||||
|
@ -258,8 +235,8 @@ export class OAuth2ProviderService {
|
|||
|
||||
fastify.post('/token', { preHandler: upload.none() }, async (request, reply) => {
|
||||
const body: any = request.body || request.query;
|
||||
const isMastodon = body.client_id?.includes('mastodon');
|
||||
|
||||
// Handle client_credentials grant type
|
||||
if (body.grant_type === "client_credentials") {
|
||||
const ret = {
|
||||
access_token: uuid(),
|
||||
|
@ -273,52 +250,56 @@ export class OAuth2ProviderService {
|
|||
|
||||
let client_id: any = body.client_id;
|
||||
const BASE_URL = `${request.protocol}://${request.hostname}`;
|
||||
const client = getClient(BASE_URL, '');
|
||||
let token = body.code || null;
|
||||
|
||||
// Normalize client_id
|
||||
if (client_id instanceof Array) {
|
||||
client_id = client_id.toString();
|
||||
} else if (!client_id) {
|
||||
client_id = null;
|
||||
}
|
||||
|
||||
// PKCE Validation for authorization_code grant
|
||||
if (body.grant_type === 'authorization_code') {
|
||||
if (!body.code_verifier) {
|
||||
reply.code(400).send({
|
||||
error: 'invalid_request',
|
||||
error_description: 'code_verifier is required for authorization_code grant type'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the stored client information matches
|
||||
const storedClientInfo = await this.#clientCache.get(`client:${body.code_verifier}`);
|
||||
if (!storedClientInfo) {
|
||||
reply.code(400).send({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Invalid authorization code'
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let finalToken = token;
|
||||
if (finalToken && finalToken.includes('-')) {
|
||||
finalToken = token;
|
||||
} else if (finalToken) {
|
||||
const matches = finalToken.match(/([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/);
|
||||
if (matches) {
|
||||
finalToken = `${matches[1]}-${matches[2]}-${matches[3]}-${matches[4]}-${matches[5]}`;
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient(BASE_URL, '', isMastodon ? 'mastodon' : 'misskey');
|
||||
|
||||
const atData = await client.fetchAccessToken(
|
||||
client_id,
|
||||
body.client_secret,
|
||||
token || "",
|
||||
finalToken || "",
|
||||
{
|
||||
scope: body.scope || "read write follow push",
|
||||
grant_type: body.grant_type,
|
||||
redirect_uri: body.redirect_uri,
|
||||
code_verifier: body.code_verifier,
|
||||
}
|
||||
);
|
||||
const ret = {
|
||||
|
||||
reply.send({
|
||||
access_token: atData.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: body.scope || "read write follow push",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
reply.send(ret);
|
||||
});
|
||||
} catch (err: any) {
|
||||
reply.code(401).send(err.response.data);
|
||||
this.#logger.error('Token exchange failed', {
|
||||
error: err,
|
||||
clientId: client_id,
|
||||
code: token
|
||||
});
|
||||
|
||||
reply.code(401).send(err.response?.data || {
|
||||
error: 'invalid_grant',
|
||||
error_description: 'The provided authorization grant is invalid'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -340,8 +321,6 @@ export class OAuth2ProviderService {
|
|||
if (query.state) param += `&state=${query.state}`;
|
||||
if (query.redirect_uri) param += `&redirect_uri=${query.redirect_uri}`;
|
||||
const client = query.client_id ? query.client_id : "";
|
||||
reply.redirect(
|
||||
`${Buffer.from(client.toString(), 'base64').toString()}?${param}`,
|
||||
);
|
||||
reply.redirect(`${Buffer.from(client.toString(), 'base64').toString()}?${param}`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue