mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-26 20:07:33 -07:00
Merge remote-tracking branch 'johann150/mk/bearer-authentication' into develop
This commit is contained in:
commit
53e6fbfb89
8 changed files with 82 additions and 30 deletions
|
@ -43,7 +43,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) {
|
||||||
|
@ -80,11 +81,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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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,6 +76,19 @@ 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,
|
||||||
|
@ -79,14 +97,8 @@ export function genOpenapiSpec() {
|
||||||
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://github.com/misskey-dev/misskey/blob/develop/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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -63,7 +63,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);
|
||||||
}
|
}
|
||||||
|
@ -71,6 +70,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 => {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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 ? { 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 ? { 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();
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,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);
|
||||||
|
@ -79,6 +78,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: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
||||||
|
|
Loading…
Reference in a new issue