213 lines
8.4 KiB
JavaScript
213 lines
8.4 KiB
JavaScript
import { secureRndstr } from "../../../../misc/secure-rndstr.js";
|
|
import { OAuthApps, OAuthTokens } from "../../../../models/index.js";
|
|
import { genId } from "../../../../misc/gen-id.js";
|
|
import { fetchMeta } from "../../../../misc/fetch-meta.js";
|
|
import { MastoApiError } from "../middleware/catch-errors.js";
|
|
import { difference, toSingleLast, unique } from "../../../../prelude/array.js";
|
|
export class AuthHelpers {
|
|
static async registerApp(ctx) {
|
|
const body = ctx.request.body || ctx.request.query;
|
|
const scopes = (typeof body.scopes === "string" ? body.scopes.split(' ') : body.scopes) ?? [
|
|
'read'
|
|
];
|
|
const redirect_uris = body.redirect_uris?.split('\n');
|
|
const client_name = body.client_name;
|
|
const website = body.website;
|
|
if (client_name == null) throw new MastoApiError(400, 'Missing client_name param');
|
|
if (redirect_uris == null || redirect_uris.length < 1) throw new MastoApiError(400, 'Missing redirect_uris param');
|
|
try {
|
|
redirect_uris.every((u)=>this.validateRedirectUri(u));
|
|
} catch {
|
|
throw new MastoApiError(400, 'Invalid redirect_uris');
|
|
}
|
|
const app = await OAuthApps.insert({
|
|
id: genId(),
|
|
clientId: secureRndstr(32),
|
|
clientSecret: secureRndstr(32),
|
|
createdAt: new Date(),
|
|
name: client_name,
|
|
website: website,
|
|
scopes: scopes,
|
|
redirectUris: redirect_uris
|
|
}).then((x)=>OAuthApps.findOneByOrFail(x.identifiers[0]));
|
|
return {
|
|
id: app.id,
|
|
name: app.name,
|
|
website: app.website,
|
|
redirect_uri: app.redirectUris.join('\n'),
|
|
client_id: app.clientId,
|
|
client_secret: app.clientSecret,
|
|
vapid_key: await fetchMeta().then((meta)=>meta.swPublicKey)
|
|
};
|
|
}
|
|
static async getAuthCode(ctx) {
|
|
const user = ctx.miauth[0];
|
|
if (!user) throw new MastoApiError(401, "Unauthorized");
|
|
const body = ctx.request.body;
|
|
const scopes = (typeof body.scopes === "string" ? body.scopes.split(' ') : body.scopes) ?? [
|
|
'read'
|
|
];
|
|
const clientId = toSingleLast(body.client_id);
|
|
if (clientId == null) throw new MastoApiError(400, "Invalid client_id");
|
|
const app = await OAuthApps.findOneBy({
|
|
clientId: clientId
|
|
});
|
|
this.validateRedirectUri(body.redirect_uri);
|
|
if (!app) throw new MastoApiError(400, "Invalid client_id");
|
|
if (!scopes.every((p)=>app.scopes.includes(p))) throw new MastoApiError(400, "Cannot request more scopes than application");
|
|
if (!app.redirectUris.includes(body.redirect_uri)) throw new MastoApiError(400, "Redirect URI not in list");
|
|
const token = await OAuthTokens.insert({
|
|
id: genId(),
|
|
active: false,
|
|
code: secureRndstr(32),
|
|
token: secureRndstr(32),
|
|
appId: app.id,
|
|
userId: user.id,
|
|
createdAt: new Date(),
|
|
scopes: scopes,
|
|
redirectUri: body.redirect_uri
|
|
}).then((x)=>OAuthTokens.findOneByOrFail(x.identifiers[0]));
|
|
return {
|
|
code: token.code
|
|
};
|
|
}
|
|
static async getAppInfo(ctx) {
|
|
const body = ctx.request.body;
|
|
const clientId = toSingleLast(body.client_id);
|
|
if (clientId == null) throw new MastoApiError(400, "Invalid client_id");
|
|
const app = await OAuthApps.findOneBy({
|
|
clientId: clientId
|
|
});
|
|
if (!app) throw new MastoApiError(400, "Invalid client_id");
|
|
return {
|
|
name: app.name
|
|
};
|
|
}
|
|
static async getAuthToken(ctx) {
|
|
const body = ctx.request.body || ctx.request.query;
|
|
const scopes = (typeof body.scope === "string" ? body.scope.split(' ') : body.scope) ?? [
|
|
'read'
|
|
];
|
|
const clientId = toSingleLast(body.client_id);
|
|
const code = toSingleLast(body.code);
|
|
const invalidScopeError = new MastoApiError(400, "invalid_scope", "The requested scope is invalid, unknown, or malformed.");
|
|
const invalidClientError = new MastoApiError(401, "invalid_client", "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.");
|
|
if (clientId == null) throw invalidClientError;
|
|
if (code == null) throw new MastoApiError(401, "Invalid code");
|
|
const app = await OAuthApps.findOneBy({
|
|
clientId: clientId
|
|
});
|
|
const token = await OAuthTokens.findOneBy({
|
|
code: code
|
|
});
|
|
this.validateRedirectUri(body.redirect_uri);
|
|
if (body.grant_type !== 'authorization_code') throw new MastoApiError(400, "Invalid grant_type");
|
|
if (!app || body.client_secret !== app.clientSecret) throw invalidClientError;
|
|
if (!token || app.id !== token.appId) throw new MastoApiError(401, "Invalid code");
|
|
if (difference(scopes, app.scopes).length > 0) throw invalidScopeError;
|
|
if (!app.redirectUris.includes(body.redirect_uri)) throw new MastoApiError(400, "Redirect URI not in list");
|
|
await OAuthTokens.update(token.id, {
|
|
active: true
|
|
});
|
|
return {
|
|
"access_token": token.token,
|
|
"token_type": "Bearer",
|
|
"scope": token.scopes.join(' '),
|
|
"created_at": Math.floor(token.createdAt.getTime() / 1000)
|
|
};
|
|
}
|
|
static async revokeAuthToken(ctx) {
|
|
const error = new MastoApiError(403, "unauthorized_client", "You are not authorized to revoke this token");
|
|
const body = ctx.request.body || ctx.request.query;
|
|
const clientId = toSingleLast(body.client_id);
|
|
const clientSecret = toSingleLast(body.client_secret);
|
|
const token = toSingleLast(body.token);
|
|
if (clientId == null || clientSecret == null || token == null) throw error;
|
|
const app = await OAuthApps.findOneBy({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret
|
|
});
|
|
const oatoken = await OAuthTokens.findOneBy({
|
|
token: token
|
|
});
|
|
if (!app || !oatoken || app.id !== oatoken.appId) throw error;
|
|
await OAuthTokens.delete(oatoken.id);
|
|
return {};
|
|
}
|
|
static async verifyAppCredentials(ctx) {
|
|
console.log(ctx.appId);
|
|
if (!ctx.appId) throw new MastoApiError(401, "The access token is invalid");
|
|
const app = await OAuthApps.findOneByOrFail({
|
|
id: ctx.appId
|
|
});
|
|
return {
|
|
name: app.name,
|
|
website: app.website,
|
|
vapid_key: await fetchMeta().then((meta)=>meta.swPublicKey ?? undefined)
|
|
};
|
|
}
|
|
static validateRedirectUri(redirectUri) {
|
|
const error = new MastoApiError(400, "Invalid redirect_uri");
|
|
if (redirectUri == null) throw error;
|
|
if (redirectUri === 'urn:ietf:wg:oauth:2.0:oob') return;
|
|
try {
|
|
const url = new URL(redirectUri);
|
|
if ([
|
|
"javascript:",
|
|
"file:",
|
|
"data:",
|
|
"mailto:",
|
|
"tel:"
|
|
].includes(url.protocol)) throw error;
|
|
} catch {
|
|
throw error;
|
|
}
|
|
}
|
|
static readScopes = [
|
|
"read:accounts",
|
|
"read:blocks",
|
|
"read:bookmarks",
|
|
"read:favourites",
|
|
"read:filters",
|
|
"read:follows",
|
|
"read:lists",
|
|
"read:mutes",
|
|
"read:notifications",
|
|
"read:search",
|
|
"read:statuses"
|
|
];
|
|
static writeScopes = [
|
|
"write:accounts",
|
|
"write:blocks",
|
|
"write:bookmarks",
|
|
"write:conversations",
|
|
"write:favourites",
|
|
"write:filters",
|
|
"write:follows",
|
|
"write:lists",
|
|
"write:media",
|
|
"write:mutes",
|
|
"write:notifications",
|
|
"write:reports",
|
|
"write:statuses"
|
|
];
|
|
static followScopes = [
|
|
"read:follows",
|
|
"read:blocks",
|
|
"read:mutes",
|
|
"write:follows",
|
|
"write:blocks",
|
|
"write:mutes"
|
|
];
|
|
static expandScopes(scopes) {
|
|
const res = [];
|
|
for (const scope of scopes){
|
|
if (scope === "read") res.push(...this.readScopes);
|
|
else if (scope === "write") res.push(...this.writeScopes);
|
|
else if (scope === "follow") res.push(...this.followScopes);
|
|
res.push(scope);
|
|
}
|
|
return unique(res);
|
|
}
|
|
}
|