User:Chlod/OAuth

From mediawiki.org

OAuth/For Developers is a little too summarized for anyone who wants an in-depth explanation of how OAuth works in the context of MediaWiki. This document attempts to break it down and explain most details without being too complicated.

Before starting[edit]

Do note that a lot of libraries already exist to support standard OAuth authentication flows. There's no need to reinvent the wheel unless you really have to, such as if you're on space or bandwidth constraints (such as in an on-wiki userscript). If you can, use those libraries, as they likely have had much more testing and quality checking prior to being released.

Glossary[edit]

This is a short glossary of terms for newer developers. Note that some of the explanations here are massively oversimplified. Cryptography is a very complex field, so some level of simplification is required to make concepts easier to understand.

OAuth 1.0a[edit]

OAuth 1.0a requires you to provide OAuth information using the Authorization HTTP header. As a reference, we're going to use the English Wikipedia API for OAuth authorization. We'll assume that we're working with a non-owner-only application with no provided public RSA key on consumer registration.

Three-legged OAuth[edit]

Three-legged OAuth is used whenever creating a non-owner-only application. It's called "three-legged" since it consists of three steps.

Step 1: Initiate[edit]

To initiate authorization, make a GET request to Special:OAuth/initiate using the non-nice URL (for example, https://en.wikipedia.org/w/index.php?title=Special:OAuth/initiate) with an OAuth authorization header. You should use an OAuth library to do these steps automatically, but a low-level representation is provided below for those who prefer to work with plain JavaScript or curl.

JavaScript[edit]

You can use the oauth-1.0a npm package for automatically signing your OAuth requests. This eases the process of making signed requests.

// Partially adapted from the oauth-1.0a documentation.
// https://github.com/ddo/oauth-1.0a#README
// Licensed under the MIT license.

const axios = require("axios");      // request library
const crypto = require("crypto");    // cryptography library
const OAuth = require("oauth-1.0a"); // OAuth library
 
const oauth = OAuth({
    consumer: { key: CONSUMER_KEY, secret: CONSUMER_SECRET },
    signature_method: "HMAC-SHA1",
    hash_function(base_string, key) {
        return crypto
            .createHmac("sha1", key)
            .update(base_string)
            .digest("base64")
    },
});

const request = {
    "method": "GET",
    "url": "https://en.wikipedia.org/w/index.php?title=Special:OAuth/initiate"
};

axios(request.url, {
    headers: {
        "Authorization": oauth.toHeader(request)
    }
});
Low-level[edit]
This section contains complicated information about OAuth that you probably won't need if you simply use the library in the section above.
Extended content
The OAuth authorization header looks like the following:
OAuth oauth_consumer_key="XXXXX",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1640138239",oauth_nonce="44938yXv2GT",oauth_callback="oob",oauth_signature="..."
It consists of the following parts:
  • OAuth – a mandatory string at the start of the header, indicating that this is an OAuth authorization header.
  • oauth_consumer_key="XXXXX" – your application's consumer key. This is shown only once when registering your application on Special:OAuthConsumerRegistration/propose.
  • oauth_signature_method="HMAC-SHA1" – indicates the signature method used to generate the OAuth signature (provided in a later field). Since we set the application up without providing a public RSA key, this will be set to HMAC-SHA1, the automatic method selected.
  • oauth_timestamp="1640138239" – the current UNIX timestamp as of the time the message was made and signed.
  • oauth_nonce="44938yXv2GT" – a randomly-generated string of characters which prevent you from accidentally re-initiating an OAuth authorization (such as when a user retries a failed web request). You should always change this whenever making a new request!
  • oauth_callback="oob" – the callback URL to send the user to after authorization. If during consumer registration, this was set to a constant URL (and not as a prefix), the value must be set to "oob". Otherwise, it should be set to the callback URL you wish to send the user to after authorizing.
  • oauth_signature="..." – an HMAC-SHA1 hash of all signed values. We'll get into that below.

The HMAC-SHA1 hash is a hashed representation of a string called the "signature base string". This string contains information about the request you're making and the OAuth consumer information as well. If your hash does not compute properly (possibly due to an incorrectly-computed hash), your authorization request will not begin.

The signature base string is made with the HTTP request type, the HTTP URL, and the "parameter string". The parameter string is a string which contains all values to be signed, including query parameters and all OAuth fields. The following JavaScript function generates a parameter string.
// Turns symbols like "#" into "%23". Also called a "URL/URI encode".
function percentEncode(str) {
    return encodeURIComponent(str)
        .replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16)}`);
}

function generateParameterString(parameters) {
    return Object.entries(parameters).map(([key, value]) => 
        percentEncode(key) + "=" + percentEncode(value)
    ).join("&");
}

generateParameterString({
    oauth_consumer_key: "XXXXX",
    oauth_signature_method: "HMAC-SHA1",
    // ...
});
After creating the parameter string, you can then create the signature base string by combining the HTTP method, the (percent-encoded) URL, and the percent-encode parameter string.
function getSignatureBaseString(method, url, parameterString) {
    return method + "&" + percentEncode(url) + "&" + percentEncode(parameterString);
}
When you're done, the signature base string should look like this:
GET&https%3A%2F%2Fen.wikipedia.org%2Fw%2Findex.php%3Ftitle%3DSpecial%3AOAuth%2Finitiate&oauth_consumer_key%3DXXXXX%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1640138239%26oauth_nonce%3D44938yXv2GT%26oauth_callback%3Doob
and if your consumer key was YYYYY, the HMAC-SHA1 signature should be a2bc0052096346fe58c2a5e8ed3e693ddc4009.

Step 2: Authorize[edit]

Step 1 should have given you a request token and secret in the token and key fields, respectively. From here, you need to redirect the user to a Special:OAuth/authorize, with the oauth_consumer_key query parameter set as your consumer key and oauth_token set as the token provided in the previous request. The user should see a confirmation dialog showing them what data is being requested and some basic information about the app.

If they choose not to give access, they are informed by Special:OAuth. If they gave access, you can proceed to Step 3.

Step 3: Getting the token[edit]

The user will be redirect to the callback URL provided with an oauth_verifier query parameter. You now need to sign another request, this time to https://en.wikipedia.org/w/index.php?title=Special:OAuth/token, with the request token and request secret set as your OAuth access token and token secret, and the oauth_verifier parameter.

MediaWiki will respond with the new access token and secret that you need for whatever requests you'll be making next, such as those with the Action API. All of those requests must be signed much like how Step 3 signs the request to get the OAuth token.

JavaScript[edit]

This picks up from the code used in Step 1.

const request = {
    method: "GET",
    url: "https://en.wikipedia.org/w/index.php?title=Special:OAuth/token",
    data: {
        "oauth_verifier": "ZZZZZ" // Provided as a query parameter
    }
};

const token = {
    key: "AAAAA",   // From data in Step 2
    secret: "BBBBB" // From data in Step 2
};

axios(request.url, {
    headers: {
        "Authorization": oauth.toHeader(request, token)
    }
});

OAuth 2.0[edit]

OAuth 2.0 is the preferred method of employing OAuth with a MediaWiki instance, as it requires less steps and simplifies the flow for developers. The concept of an access secret is removed, and only the access token is required for authenticating a user. OAuth 2 tokens, however, have a very short expiry time and need to be refreshed with a "refresh token" when they expire. This, however, gets rid of the need to reauthorize every once-in-a-while for users.

OAuth 2.0 also introduces the concept of confidential and public (non-confidential) consumers. Confidential consumers can keep their client secret a secret, however public consumers cannot. Public consumers are usually found in embedded systems or mobile applications, where the system where the secret is located can be cracked open and stolen. This section will discuss confidential consumers, see User:Chlod/OAuth § OAuth 2.0 (public) for the section on public consumers.

The MediaWiki OAuth 2.0 flow runs on the REST API instead of on Special pages. Unlike other REST API endpoints, it is not prefixed with a version identifier. Like with OAuth 1.0a, we'll use the English Wikipedia as an example for the authorization flow.

Step 1: Authorization[edit]

Send the user to https://en.wikipedia.org/w/rest.php/oauth2/authorize with the following query parameters:

  • response_type set to code. This tells MediaWiki you're looking to get an authorization code.
  • client_id set to your application key (i.e. consumer key).
  • (optional but recommended) state set to a random string. When you receive your code in Step 2, you need to check if the state matches, or else you might open a user up to a CSRF attack. Consider the birthday problem when picking the length of the random string.
  • (optional) redirect_uri set to the URL to redirect to (depending on how you set up the callback URL when registering the consumer).

The user, should they choose to authorize the application, will then be sent to your callback URL for step 2.

Step 2: Get the access token[edit]

When the user is redirected back to your application, you will be provided a code parameter and the state (if provided) that was provided in Step 1.

First and foremost, check if the state matches with the state you stored. If it does, then this is likely a genuine request from the user.

The provided code is called the authorization code, and is used to get the access token and refresh token for a user. To trade in your authorization code for an access token, you'll need to make another REST API request, this time a POST request to https://en.wikipedia.org/w/rest.php/oauth2/access_token, containing the following body parameters (as application/x-www-form-urlencoded, not as query parameters).

  • client_id set to your application key.
  • client_secret set to your application secret.
  • grant_type set to authorization_code. This tells MediaWiki you're asking for an access token with an authorization code.
  • code set to your authorization code.
  • (optional) redirect_uri set to the redirect URI you used in Step 1. They must match or else MediaWiki will complain.

You should receive a JSON response with the access token (as access_token) and refresh token (as refresh_token). You'll also receive the date on which the access token will expire (as expires_in, provided as a UNIX timestamp), after which you'll need to request a new access token using the refresh token.

Step 2.5: Refreshing the token[edit]

Although this isn't really part of the authorization flow, it still needs to be part of your application if you plan to reuse the tokens at some point. Since the access token will expire at some point, you'll need to refresh the token to get a new access token and refresh token.

To do this, do the same thing as in Step 2 except set the grant_type to refresh_token instead and set refresh_token to your refresh token. Obviously, you will no longer be providing a code in your request.

You should receive a response identical to the one used in Step 2.

Step 3: Making requests[edit]

To make requests on the user's behalf, send requests to the wiki's Action API (i.e. api.php) with the access token in the Bearer slot.

axios("https://en.wikipedia.org/w/api.php", {
    data: {/* ... */}, 
    headers: {
        "Authorization": "Bearer " + access_token
    }
});

OAuth 2.0 (public)[edit]