mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-02 06:39:46 +08:00
fix(server): send User-Agent on GitHub OAuth fetches (#15524)
## Description Self-hosted GitHub OAuth login fails at token exchange because the outbound request to `https://github.com/login/oauth/access_token` has no `User-Agent` header. GitHub then returns 403 ("Request forbidden by administrative rules"), which is surfaced as `INVALID_OAUTH_CALLBACK_CODE`. OAuth `safeFetch` only forwarded `authorization`, `content-type`, and `accept`, so even a User-Agent on the request would be stripped. This change: - allows `user-agent` in OAuth `fetchOptions()` - always sends `User-Agent: AFFiNE-Server` from `fetchJson()` (covers token exchange and `api.github.com` user/email fetches) Fixes #15521 ## Checklist - [x] The PR targets the `canary` branch and its title follows Conventional Commits - [x] Tests are added or updated where it makes sense <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved OAuth request compatibility by including a standard `User-Agent` header. * Ensured the header is permitted consistently during OAuth token exchanges. * **Tests** * Added coverage to verify case-insensitive handling of the `User-Agent` header in GitHub OAuth requests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> ---------
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import serverNativeModule from '@affine/server-native';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
type NativeSafeFetchRequest = {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
allowedHeaders?: string[];
|
||||
};
|
||||
|
||||
const test = ava.serial as TestFn<{
|
||||
requests: NativeSafeFetchRequest[];
|
||||
}>;
|
||||
|
||||
function headerValue(
|
||||
headers: Record<string, string> | undefined,
|
||||
name: string
|
||||
) {
|
||||
const target = name.toLowerCase();
|
||||
for (const [key, value] of Object.entries(headers ?? {})) {
|
||||
if (key.toLowerCase() === target) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
test.before(t => {
|
||||
const requests: NativeSafeFetchRequest[] = [];
|
||||
|
||||
Sinon.stub(serverNativeModule, 'safeFetch').callsFake(async request => {
|
||||
const nativeRequest = request as NativeSafeFetchRequest;
|
||||
requests.push(nativeRequest);
|
||||
return {
|
||||
status: 200,
|
||||
finalUrl: nativeRequest.url,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(
|
||||
JSON.stringify({
|
||||
access_token: 'github-access-token',
|
||||
scope: 'read:user user:email',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
t.context.requests = requests;
|
||||
});
|
||||
|
||||
test.after.always(() => {
|
||||
Sinon.restore();
|
||||
});
|
||||
|
||||
test('github oauth token exchange should send a User-Agent header', async t => {
|
||||
const { OAuthProviderName } = await import('../../plugins/oauth/config');
|
||||
const { OAuthProvider } = await import('../../plugins/oauth/providers/def');
|
||||
|
||||
class Probe extends OAuthProvider {
|
||||
override provider = OAuthProviderName.GitHub;
|
||||
|
||||
getAuthUrl() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
return { accessToken: 'token' };
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
return { id: 'id', email: 'user@example.com' };
|
||||
}
|
||||
|
||||
exchange() {
|
||||
return this.postFormJson(
|
||||
'https://github.com/login/oauth/access_token',
|
||||
'code=oauth-code'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { requests } = t.context;
|
||||
requests.length = 0;
|
||||
|
||||
await new Probe().exchange();
|
||||
|
||||
t.is(requests.length, 1);
|
||||
t.is(requests[0].url, 'https://github.com/login/oauth/access_token');
|
||||
t.truthy(headerValue(requests[0].headers, 'user-agent'));
|
||||
t.true(
|
||||
(requests[0].allowedHeaders ?? []).some(
|
||||
header => header.toLowerCase() === 'user-agent'
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -85,7 +85,7 @@ export abstract class OAuthProvider {
|
||||
timeoutMs: 10_000,
|
||||
maxRedirects: 3,
|
||||
maxBytes: 1024 * 1024,
|
||||
allowedHeaders: ['authorization', 'content-type', 'accept'],
|
||||
allowedHeaders: ['authorization', 'content-type', 'accept', 'user-agent'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,7 +96,14 @@ export abstract class OAuthProvider {
|
||||
) {
|
||||
const response = await safeFetch(
|
||||
url,
|
||||
{ ...init, headers: { ...init?.headers, Accept: 'application/json' } },
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'AFFiNE-Server',
|
||||
},
|
||||
},
|
||||
this.fetchOptions(url)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user