Merge bearer-authentication

foundKeyGang/foundKey#15
This commit is contained in:
Johann150 2022-07-20 15:10:47 +02:00
commit f3e196528f
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
8 changed files with 84 additions and 32 deletions

View file

@ -34,7 +34,8 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
}; };
// Authentication // Authentication
authenticate(body['i']).then(([user, app]) => { // for GET requests, do not even pass on the body parameter as it is considered unsafe
authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => {
// API invoking // API invoking
call(endpoint.name, user, app, body, ctx).then((res: any) => { call(endpoint.name, user, app, body, ctx).then((res: any) => {
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
@ -46,11 +47,15 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
}); });
}).catch(e => { }).catch(e => {
if (e instanceof AuthenticationError) { if (e instanceof AuthenticationError) {
reply(403, new ApiError({ ctx.response.status = 403;
message: 'Authentication failed. Please ensure your token is correct.', ctx.response.set('WWW-Authenticate', 'Bearer');
ctx.response.body = {
message: 'Authentication failed: ' + e.message,
code: 'AUTHENTICATION_FAILED', code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
})); kind: 'client',
};
res();
} else { } else {
reply(500, new ApiError()); reply(500, new ApiError());
} }

View file

@ -15,8 +15,25 @@ export class AuthenticationError extends Error {
} }
} }
export default async (token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
if (token == null) { let token: string | null = null;
// check if there is an authorization header set
if (authorization != null) {
if (bodyToken != null) {
throw new AuthenticationError('using multiple authorization schemes');
}
// check if OAuth 2.0 Bearer tokens are being used
// Authorization schemes are case insensitive
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
token = authorization.substring(7);
} else {
throw new AuthenticationError('unsupported authentication scheme');
}
} else if (bodyToken != null) {
token = bodyToken;
} else {
return [null, null]; return [null, null];
} }
@ -25,7 +42,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>); () => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
if (user == null) { if (user == null) {
throw new AuthenticationError('user not found'); throw new AuthenticationError('unknown token');
} }
return [user, null]; return [user, null];
@ -39,7 +56,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
}); });
if (accessToken == null) { if (accessToken == null) {
throw new AuthenticationError('invalid signature'); throw new AuthenticationError('unknown token');
} }
AccessTokens.update(accessToken.id, { AccessTokens.update(accessToken.id, {

View file

@ -15,7 +15,7 @@ export function genOpenapiSpec() {
externalDocs: { externalDocs: {
description: 'Repository', description: 'Repository',
url: 'https://github.com/misskey-dev/misskey', url: 'https://akkoma.dev/FoundKeyGang/FoundKey',
}, },
servers: [{ servers: [{
@ -33,6 +33,11 @@ export function genOpenapiSpec() {
in: 'body', in: 'body',
name: 'i', name: 'i',
}, },
// TODO: change this to oauth2 when the remaining oauth stuff is set up
Bearer: {
type: 'http',
scheme: 'bearer',
}
}, },
}, },
}; };
@ -71,22 +76,29 @@ export function genOpenapiSpec() {
schema.required.push('file'); schema.required.push('file');
} }
const security = [
{
ApiKeyAuth: [],
},
{
Bearer: [],
},
];
if (!endpoint.meta.requireCredential) {
// add this to make authentication optional
security.push({});
}
const info = { const info = {
operationId: endpoint.name, operationId: endpoint.name,
summary: endpoint.name, summary: endpoint.name,
description: desc, description: desc,
externalDocs: { externalDocs: {
description: 'Source code', description: 'Source code',
url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, url: `https://akkoma.dev/FoundKeyGang/FoundKey/src/branch/main/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
}, },
...(endpoint.meta.tags ? { tags: endpoint.meta.tags || undefined,
tags: [endpoint.meta.tags[0]], security,
} : {}),
...(endpoint.meta.requireCredential ? {
security: [{
ApiKeyAuth: [],
}],
} : {}),
requestBody: { requestBody: {
required: true, required: true,
content: { content: {
@ -181,9 +193,16 @@ export function genOpenapiSpec() {
}, },
}; };
spec.paths['/' + endpoint.name] = { const path = {
post: info, post: info,
}; };
if (endpoint.meta.allowGet) {
path.get = { ...info };
// API Key authentication is not permitted for GET requests
path.get.security = path.get.security.filter(elem => !Object.prototype.hasOwnProperty.call(elem, 'ApiKeyAuth'));
}
spec.paths['/' + endpoint.name] = path;
} }
return spec; return spec;

View file

@ -17,10 +17,14 @@ export const initializeStreamingServer = (server: http.Server) => {
ws.on('request', async (request) => { ws.on('request', async (request) => {
const q = request.resourceURL.query as ParsedUrlQuery; const q = request.resourceURL.query as ParsedUrlQuery;
// TODO: トークンが間違ってるなどしてauthenticateに失敗したら const [user, app] = await authenticate(request.httpRequest.headers.authorization, q.i)
// コネクション切断するなりエラーメッセージ返すなりする .catch(err => {
// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので) request.reject(403, err.message);
const [user, app] = await authenticate(q.i as string); return [];
});
if (typeof user === 'undefined') {
return;
}
if (user?.isSuspended) { if (user?.isSuspended) {
request.reject(400); request.reject(400);

View file

@ -62,7 +62,6 @@ const ok = async () => {
croppedCanvas.toBlob(blob => { croppedCanvas.toBlob(blob => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', blob); formData.append('file', blob);
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) { if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder); formData.append('folderId', defaultStore.state.uploadFolder);
} }
@ -70,6 +69,9 @@ const ok = async () => {
fetch(apiUrl + '/drive/files/create', { fetch(apiUrl + '/drive/files/create', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: {
authorization: `Bearer ${$i.token}`,
},
}) })
.then(response => response.json()) .then(response => response.json())
.then(f => { .then(f => {

View file

@ -54,7 +54,6 @@ export default defineComponent({
canvas.toBlob(blob => { canvas.toBlob(blob => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', blob); formData.append('file', blob);
formData.append('i', this.$i.token);
if (this.$store.state.uploadFolder) { if (this.$store.state.uploadFolder) {
formData.append('folderId', this.$store.state.uploadFolder); formData.append('folderId', this.$store.state.uploadFolder);
} }
@ -62,6 +61,9 @@ export default defineComponent({
fetch(apiUrl + '/drive/files/create', { fetch(apiUrl + '/drive/files/create', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: {
authorization: `Bearer ${this.$i.token}`,
},
}) })
.then(response => response.json()) .then(response => response.json())
.then(f => { .then(f => {

View file

@ -23,17 +23,16 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
pendingApiRequestsCount.value--; pendingApiRequestsCount.value--;
}; };
const promise = new Promise((resolve, reject) => { const authorizationToken = token ?? $i?.token ?? undefined;
// Append a credential const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined;
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
// Send request const promise = new Promise((resolve, reject) => {
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
credentials: 'omit', credentials: 'omit',
cache: 'no-cache', cache: 'no-cache',
headers: { authorization },
}).then(async (res) => { }).then(async (res) => {
const body = res.status === 204 ? null : await res.json(); const body = res.status === 204 ? null : await res.json();
@ -52,7 +51,7 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
return promise; return promise;
}) as typeof apiClient.request; }) as typeof apiClient.request;
export const apiGet = ((endpoint: string, data: Record<string, any> = {}) => { export const apiGet = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
pendingApiRequestsCount.value++; pendingApiRequestsCount.value++;
const onFinally = () => { const onFinally = () => {
@ -61,12 +60,16 @@ export const apiGet = ((endpoint: string, data: Record<string, any> = {}) => {
const query = new URLSearchParams(data); const query = new URLSearchParams(data);
const authorizationToken = token ?? $i?.token ?? undefined;
const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined;
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
// Send request // Send request
fetch(`${apiUrl}/${endpoint}?${query}`, { fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET', method: 'GET',
credentials: 'omit', credentials: 'omit',
cache: 'default', cache: 'default',
headers: { authorization },
}).then(async (res) => { }).then(async (res) => {
const body = res.status === 204 ? null : await res.json(); const body = res.status === 204 ? null : await res.json();

View file

@ -70,7 +70,6 @@ export function uploadFile(
} }
const formData = new FormData(); const formData = new FormData();
formData.append('i', $i.token);
formData.append('force', 'true'); formData.append('force', 'true');
formData.append('file', resizedImage || file); formData.append('file', resizedImage || file);
formData.append('name', ctx.name); formData.append('name', ctx.name);
@ -78,6 +77,7 @@ export function uploadFile(
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true); xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.setRequestHeader('Authorization', `Bearer ${$i.token}`);
xhr.onload = (ev) => { xhr.onload = (ev) => {
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
// TODO: 消すのではなくて再送できるようにしたい // TODO: 消すのではなくて再送できるようにしたい