diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index b0fdc558e6..677a85473f 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -4,7 +4,8 @@ import fastifyMiddie, { IncomingMessageExtended } from '@fastify/middie';
 import { JSDOM } from 'jsdom';
 import parseLinkHeader from 'parse-link-header';
 import ipaddr from 'ipaddr.js';
-import oauth2orize, { OAuth2 } from 'oauth2orize';
+import oauth2orize, { type OAuth2 } from 'oauth2orize';
+import * as oauth2Query from 'oauth2orize/lib/response/query.js';
 import { bindThis } from '@/decorators.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
@@ -339,8 +340,17 @@ export class OAuth2ProviderService {
 			scopes: string[],
 		}> = {};
 
+		const query = (txn, res, params) => {
+			// RFC 9207
+			// TODO: Oh no, perhaps returning to oidc-provider is better. Hacks everywhere here.
+			params.iss = config.url;
+			oauth2Query.default(txn, res, params);
+		};
+
 		this.#server.grant(oauth2Pkce.extensions());
-		this.#server.grant(oauth2orize.grant.code((client, redirectUri, token, ares, areq, done) => {
+		this.#server.grant(oauth2orize.grant.code({
+			modes: { query }
+		}, (client, redirectUri, token, ares, areq, done) => {
 			(async (): Promise<OmitFirstElement<Parameters<typeof done>>> => {
 				console.log('HIT grant code:', client, redirectUri, token, ares, areq);
 				const code = secureRndstr(32, true);
@@ -483,7 +493,7 @@ export class OAuth2ProviderService {
 				// https://indieauth.spec.indieweb.org/#authorization-request
 				// Allow same-origin redirection
 				if (redirectUrl.protocol !== clientUrl.protocol || redirectUrl.host !== clientUrl.host) {
-					// TODO: allow more redirect_uri by Client Information Discovery
+					// TODO: allow only explicit redirect_uri by Client Information Discovery
 					throw new Error('cross-origin redirect_uri is not supported yet.');
 				}
 
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts
index 66c3a970bc..5dd0d7c39f 100644
--- a/packages/backend/test/e2e/oauth.ts
+++ b/packages/backend/test/e2e/oauth.ts
@@ -6,7 +6,6 @@ import type { INestApplicationContext } from '@nestjs/common';
 import { AuthorizationCode } from 'simple-oauth2';
 import pkceChallenge from 'pkce-challenge';
 import { JSDOM } from 'jsdom';
-import { api } from '../utils.js';
 
 const clientPort = port + 1;
 const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`;
@@ -97,6 +96,7 @@ describe('OAuth', () => {
 		assert.strictEqual(location.origin + location.pathname, redirect_uri);
 		assert.ok(location.searchParams.has('code'));
 		assert.strictEqual(location.searchParams.get('state'), 'state');
+		assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); // RFC 9207
 
 		const token = await client.getToken({
 			code: location.searchParams.get('code')!,
@@ -380,6 +380,19 @@ describe('OAuth', () => {
 			assert.strictEqual(response.status, 500);
 		});
 
+		test('No redirect_uri at authorization endpoint', async () => {
+			const client = getClient();
+
+			const response = await fetch(client.authorizeURL({
+				scope: 'write:notes',
+				state: 'state',
+				code_challenge: 'code',
+				code_challenge_method: 'S256',
+			}));
+			// TODO: status code
+			assert.strictEqual(response.status, 500);
+		});
+
 		test('Invalid redirect_uri at token endpoint', async () => {
 			const { code_challenge, code_verifier } = pkceChallenge.default(128);
 
@@ -407,6 +420,32 @@ describe('OAuth', () => {
 			}));
 		});
 
+		test('No redirect_uri at token endpoint', async () => {
+			const { code_challenge, code_verifier } = pkceChallenge.default(128);
+
+			const client = getClient();
+
+			const response = await fetch(client.authorizeURL({
+				redirect_uri,
+				scope: 'write:notes',
+				state: 'state',
+				code_challenge,
+				code_challenge_method: 'S256',
+			}));
+			assert.strictEqual(response.status, 200);
+
+			const decisionResponse = await fetchDecisionFromResponse(response, alice);
+			assert.strictEqual(decisionResponse.status, 302);
+
+			const location = new URL(decisionResponse.headers.get('location')!);
+			assert.ok(location.searchParams.has('code'));
+
+			await assert.rejects(client.getToken({
+				code: location.searchParams.get('code')!,
+				code_verifier,
+			}));
+		});
+
 		// TODO: disallow random same-origin URLs with strict redirect_uris with client information discovery
 	});
 
@@ -415,4 +454,6 @@ describe('OAuth', () => {
 	// TODO: authorizing two users concurrently
 
 	// TODO: Error format required by OAuth spec
+
+	// TODO: Client Information Discovery
 });