User:Chlod/OAuth
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.
- Consumer refers to any application which uses information using OAuth.
- Nice URL refers to a formatted URL, such as https://en.wikipedia.org/wiki/Main_Page.
- Non-nice URL refers to an unformatted URL, such as https://en.wikipedia.org/w/index.php?title=Main_Page.
- RSA refers to RivestâShamirâAdleman encryption, a method of encryption which requires the use of a public key (one that can be shared with anyone and can only encrypt data) and a private key (one that you must keep secret and can be used to both encrypt and decrypt data).
- HMAC refers to hash-based message authentication code, a method of cryptography which relies on a pre-shared secret instead of keys. This (or RSA) is used to ensure that the OAuth 1.0a authorization request came from your application itself.
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]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:
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",
// ...
});
function getSignatureBaseString(method, url, parameterString) {
return method + "&" + percentEncode(url) + "&" + percentEncode(parameterString);
}
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%3Dooband 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 tocode
. 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 toauthorization_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]This page is in progress Please check back later for additional changes. |