From ccc6fb89b8d44f57d92dadf74494271eebf13ad0 Mon Sep 17 00:00:00 2001 From: Siddharth VP Date: Thu, 10 Nov 2022 22:49:37 +0530 Subject: [PATCH] Support authentication via OAuth 2 --- src/bot.ts | 9 +++- src/core.ts | 14 +++--- tests/mocking/loginCredentials.js | 4 ++ tests/oauth2.test.js | 74 +++++++++++++++++++++++++++++++ website/docs/1-getting-started.md | 9 ++-- 5 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 tests/oauth2.test.js diff --git a/src/bot.ts b/src/bot.ts index 5445f06..f112ea4 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -104,6 +104,7 @@ export interface MwnOptions { accessToken: string; accessSecret: string; }; + OAuth2AccessToken?: string; maxRetries?: number; retryPause?: number; shutoff?: { @@ -272,6 +273,7 @@ export class mwn { oauth: OAuth; usingOAuth: boolean; + usingOAuth2: boolean; static Error = MwnError; @@ -371,7 +373,7 @@ export class mwn { */ static async init(config: MwnOptions): Promise { const bot = new mwn(config); - if (bot._usingOAuth()) { + if (bot.options.OAuth2AccessToken || bot._usingOAuth()) { bot.initOAuth(); await bot.getTokensAndSiteInfo(); } else { @@ -441,6 +443,11 @@ export class mwn { * Initialize OAuth instance */ initOAuth() { + if (this.options.OAuth2AccessToken) { + this.usingOAuth2 = true; + return; + } + if (!this._usingOAuth()) { // without this, the API would return a confusing // mwoauth-invalid-authorization invalid consumer error diff --git a/src/core.ts b/src/core.ts index 2d09ed3..5a4ac25 100644 --- a/src/core.ts +++ b/src/core.ts @@ -99,8 +99,12 @@ export class Request { applyAuthentication() { let requestOptions = this.requestParams; - if (this.bot.usingOAuth) { - // OAuth authentication + + if (this.bot.usingOAuth2) { + // OAuth 2 authentication + requestOptions.headers['Authorization'] = `Bearer ${this.bot.options.OAuth2AccessToken}`; + } else if (this.bot.usingOAuth) { + // OAuth 1a authentication requestOptions.headers = { ...requestOptions.headers, ...this.makeOAuthHeader({ @@ -382,11 +386,9 @@ export class Response { this.requestOptions.retryNumber < this.bot.options.maxRetries && // ENOTFOUND usually means bad apiUrl is provided, retrying is pointless and annoying error.code !== 'ENOTFOUND' && - ( - !error.response?.status || + (!error.response?.status || // Vaguely retriable error codes - [408, 409, 425, 429, 500, 502, 503, 504].includes(error.response.status) - ) + [408, 409, 425, 429, 500, 502, 503, 504].includes(error.response.status)) ) { // error might be transient, give it another go! log(`[W] Encountered ${error}, retrying in ${this.bot.options.retryPause / 1000} seconds`); diff --git a/tests/mocking/loginCredentials.js b/tests/mocking/loginCredentials.js index 291ca89..affa4c1 100644 --- a/tests/mocking/loginCredentials.js +++ b/tests/mocking/loginCredentials.js @@ -11,6 +11,10 @@ module.exports = { username: realCredentials.username2, password: realCredentials.password2, }, + account1_oauth2: { + apiUrl: 'https://test.wikipedia.org/w/api.php', + OAuth2AccessToken: realCredentials.oauth2_access_token, + }, account1_oauth: { apiUrl: 'https://test.wikipedia.org/w/api.php', OAuthCredentials: { diff --git a/tests/oauth2.test.js b/tests/oauth2.test.js new file mode 100644 index 0000000..169b522 --- /dev/null +++ b/tests/oauth2.test.js @@ -0,0 +1,74 @@ +'use strict'; + +const { mwn, expect, verifyTokenAndSiteInfo } = require('./test_base'); + +const oauthCredentials = require('./mocking/loginCredentials.js').account1_oauth2; + +let bot = new mwn({ + ...oauthCredentials, +}); + +bot.initOAuth(); + +describe('OAuth 2', async function () { + it('gets tokens (GET request)', function () { + return bot.getTokensAndSiteInfo().then(() => { + verifyTokenAndSiteInfo(bot); + }); + }); + + it('gets a token (POST request)', function () { + return bot + .request( + { + action: 'query', + meta: 'tokens', + type: 'csrf', + }, + { + method: 'post', + } + ) + .then((data) => { + expect(data.query.tokens.csrftoken.length).to.be.gt(10); + }); + }); + + it('purges a page (has to be POST request)', function () { + return bot.purge([11791]).then(function (response) { + expect(response).to.be.instanceOf(Array); + expect(response[0].purged).to.equal(true); + }); + }); + + it('gets a token (multipart/form-data POST request)', function () { + return bot + .request( + { + action: 'query', + meta: 'tokens', + type: 'csrf', + }, + { + method: 'post', + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ) + .then((data) => { + expect(data.query.tokens.csrftoken.length).to.be.gt(10); + }); + }); + + it('gets a page text', function () { + return bot + .request({ + action: 'query', + titles: 'Main Page', + }) + .then((data) => { + expect(data.query.pages[0].title).to.be.a('string'); + }); + }); +}); diff --git a/website/docs/1-getting-started.md b/website/docs/1-getting-started.md index e7db017..3bcfac0 100644 --- a/website/docs/1-getting-started.md +++ b/website/docs/1-getting-started.md @@ -25,7 +25,7 @@ If you're migrating from mwbot, note that: #### Set up a bot password or OAuth credentials -Mwn supports authentication via both [BotPasswords](https://www.mediawiki.org/wiki/Manual:Bot_passwords) and [OAuth 1.0a](https://www.mediawiki.org/wiki/OAuth/Owner-only_consumers). Use of OAuth is recommended as it does away the need for separate API requests for logging in, and is also more secure. +Mwn supports authentication via both [BotPasswords](https://www.mediawiki.org/wiki/Manual:Bot_passwords) and [OAuth](https://www.mediawiki.org/wiki/OAuth/Owner-only_consumers). Use of OAuth is recommended as it does away the need for separate API requests for logging in, and is also more secure. Both OAuth versions, 1.0a and 2, are supported. Bot passwords, however, are a bit easier to set up. To generate one, go to the wiki's [Special:BotPasswords](https://en.wikipedia.org/wiki/Special:BotPasswords) page. @@ -39,8 +39,11 @@ const bot = await mwn.init({ username: 'YourBotUsername', password: 'YourBotPassword', - // Instead of username and password, you can use OAuth 1.0a to authenticate, + // Instead of username and password, you can use OAuth 2 to authenticate (recommended), // if the wiki has Extension:OAuth enabled + OAuth2AccessToken: "YouOAuth2AccessToken", + + // Or use OAuth 1.0a (also only applicable for wikis with Extension:OAuth) OAuthCredentials: { consumerToken: '16_DIGIT_ALPHANUMERIC_KEY', consumerSecret: '20_DIGIT_ALPHANUMERIC_KEY', @@ -58,7 +61,7 @@ const bot = await mwn.init({ }); ``` -This creates a bot instance, signs in and fetches tokens needed for editing. +This creates a bot instance, signs in and fetches tokens needed for editing. (If credentials for multiple authentication methods are provided, OAuth 2 takes precedence, followed by OAuth 1.0a and bot passwords.) You can also create a bot instance synchronously (without using await):