Skip to content

Commit

Permalink
Support authentication via OAuth 2
Browse files Browse the repository at this point in the history
  • Loading branch information
siddharthvp committed Nov 10, 2022
1 parent bc46b13 commit ccc6fb8
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 10 deletions.
9 changes: 8 additions & 1 deletion src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface MwnOptions {
accessToken: string;
accessSecret: string;
};
OAuth2AccessToken?: string;
maxRetries?: number;
retryPause?: number;
shutoff?: {
Expand Down Expand Up @@ -272,6 +273,7 @@ export class mwn {
oauth: OAuth;

usingOAuth: boolean;
usingOAuth2: boolean;

static Error = MwnError;

Expand Down Expand Up @@ -371,7 +373,7 @@ export class mwn {
*/
static async init(config: MwnOptions): Promise<mwn> {
const bot = new mwn(config);
if (bot._usingOAuth()) {
if (bot.options.OAuth2AccessToken || bot._usingOAuth()) {
bot.initOAuth();
await bot.getTokensAndSiteInfo();
} else {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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`);
Expand Down
4 changes: 4 additions & 0 deletions tests/mocking/loginCredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
74 changes: 74 additions & 0 deletions tests/oauth2.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
9 changes: 6 additions & 3 deletions website/docs/1-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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',
Expand All @@ -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):

Expand Down

0 comments on commit ccc6fb8

Please sign in to comment.