OAuth/オーナー専用コンシューマー
オーナー専用コンシューマーは、OAuthを使用しユーザー認証と権限管理を行うための一手段です。あるアプリケーションが特定の別ユーザーとしてリクエストを行う場合の複雑な認証とは異なり、この認証手段は極めて単純です。 ボットをはじめとし、常に同一のユーザーとして認証を行う場合がこれに当てはまります。 この機能を使用するには、バージョン1.27以上のExtension:OAuthがインストールされている必要があります。
OAuthプロトコルには、仕様に大きな違いのある、OAuth 1 と OAuth 2 の2つのバージョンが存在します。どちらを使用すべきか確証がない場合は、OAuth 2 を使用してください。
OAuth 2
OAuth 2 を用いたオーナー専用アプリケーションの運用方法は明快単純です。
Special:OAuthConsumerRegistration/propose/oauth2 上で「オーナー限定 (owner-only)」オプションにチェックを入れ、アプリケーションを登録してください。
(ウィキファームの場合、この特別ページは中央管理ウィキにおいてのみ利用できます。 ウィキメディアの場合、この特別ページは meta:Special:OAuthConsumerRegistration/propose/oauth2 にあります。)
その後、フォーム送信時に表示されるアクセストークンを記録し、HTTPヘッダーに Authorization: Bearer <アクセストークン> を追加することでAPIリクエストに署名します。
OAuth 1
To work as an owner-only consumer, the application must take four strings as configuration settings: the consumer key, the consumer secret, the access token and the access secret.
The user can obtain those via Special:OAuthConsumerRegistration/propose.
The option owner-only has to be checked.
(In case of a wikifarm, the special page is only available on the central wiki of the farm. In case of Wikimedia, it's at meta:Special:OAuthConsumerRegistration/propose.)
The application can then authenticate API requests by adding an Authorization header which is computed from those parameters as defined in the OAuth 1.0a standard; libraries exist in many languages to help with this.
Some libraries call this the two-legged OAuth 1.0 protocol.
The OAuth Bible more correctly calls it one-legged.
Some sources call the consumer key a "client ID", the consumer secret a "client secret", the access token just a "token", and the access secret a "token secret".
The code snippets below assume the application uses a shared secret (HMAC-SHA1) for signing (i.e., the RSA field was left empty at registration).
PHP
Using Oauthclient-php:
use MediaWiki\OAuthClient\Consumer;
use MediaWiki\OAuthClient\Token;
use MediaWiki\OAuthClient\Request;
use MediaWiki\OAuthClient\SignatureMethod\HmacSha1;
$consumer = new Consumer( $consumerKey, $consumerSecret );
$accessToken = new Token( $accessToken, $accessSecret );
$request = Request::fromConsumerAndToken( $consumer, $accessToken, 'GET', 'https://en.wikipedia.org/w/api.php', $apiParams );
$request->signRequest( new HmacSha1(), $consumer, $accessToken );
$authorizationHeader = $request->toHeader();
// Then use this header on a request, for example:
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_URL => 'https://en.wikipedia.org/w/api.php',
CURLOPT_USERAGENT => 'My_bot',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($apiParams),
CURLOPT_HTTPHEADER => [$authenticationHeader],
]);
$result = curl_exec($ch);
Using the PECL package:
$oauth = new OAuth( $consumerKey, $consumerSecret );
$oauth->setToken( $accessToken, $accessSecret );
// Generate a header, perhaps to be sent via a CURL object:
$authorizationHeader = 'Authorization: ' . $oauth->getRequestHeader( 'GET', 'https://en.wikipedia.org/w/api.php', $apiParams );
// Or simply fetch a page directly:
$oauth->fetch('https://en.wikipedia.org/w/api.php', $apiParams);
Python
Using requests_oauthlib:
import requests
from requests_oauthlib import OAuth1
auth = OAuth1(consumer_key, consumer_secret, access_token, access_secret)
requests.post(url='https://en.wikipedia.org/w/api.php', data=data, auth=auth)
Perl
Using Net::OAuth:
use Net::OAuth;
my $request = Net::OAuth->request( 'protected resource' )->new(
request_method => 'POST',
request_url => 'https://en.wikipedia.org/w/api.php',
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
token => $access_token,
token_secret => $access_secret,
signature_method => 'HMAC-SHA1',
timestamp => time(),
nonce => $nonce,
# Only if using GET or POST as application/x-www-form-urlencoded,
# omit extra_params if POSTing as multipart/form-data.
extra_params => \%params,
);
$request->sign();
my $authorizationHeader = $request->to_authorization_header();
To generate the nonce (to make the request unique), you could just do something like int( rand( 2**32 ) ), but using a random number generator such as Bytes::Random::Secure would be more secure:
use Bytes::Random::Secure ();
# This object may be kept and reused for the lifetime of the program
my $rand = Bytes::Random::Secure->new( NonBlocking => 1 );
# This generates the nonce, and replaces some characters with safer ones.
my $nonce = $rand->bytes_base64( 15 );
$nonce =~ tr!+/\n!-_!;
Awk / shell
Using GNU Awk and openssl. Function library in Wikiget.
#
# MWOAuthGenerateHeader() - MediaWiki Generate OAuth Header
#
# Credit: translation of PHP script https://www.mediawiki.org/wiki/OAuth/Owner-only_consumers#Algorithm
#
function MWOAuthGenerateHeader(consumerKey, consumerSecret, accessKey, accessSecret, url, method, data,
nonce,headerParams,dataArr,allParams,allParamsJoined,k,i,j,url2,
signatureBaseParts,signatureBaseString,hmac,header,save_sorted) {
# sort associative arrays by index string ascending (lexicographic order)
PROCINFO["sorted_in"] = "@ind_str_asc"
nonce = strip(splitx(sys2varPipe(systime() randomnumber(1000000), "openssl md5"), "= ", 2))
asplit(headerParams, "oauth_consumer_key=" consumerKey " oauth_token=" accessKey " oauth_signature_method=HMAC-SHA1 oauth_timestamp=" systime() " oauth_nonce=" nonce " oauth_version=1.0")
asplit(dataArr, data, "=", "&")
concatarray(headerParams,dataArr,allParams)
for (k in allParams)
allParamsJoined[i++] = k "=" allParams[k]
url2 = urlElement(url, "scheme") "://" tolower(urlElement(url, "netloc")) urlElement(url, "path")
asplit(signatureBaseParts, "0=" toupper(method) " 1=" url " 2=" join(allParamsJoined, 0, length(allParamsJoined) - 1, "&"))
signatureBaseString = urlencodeawk(signatureBaseParts[0], "rawphp") "&" urlencodeawk(signatureBaseParts[1], "rawphp") "&" urlencodeawk(signatureBaseParts[2], "rawphp")
hmac = sys2varPipe(signatureBaseString, "openssl sha1 -hmac " shquote(urlencodeawk(consumerSecret, "rawphp") "&" urlencodeawk(accessSecret, "rawphp")) " -binary")
headerParams["oauth_signature"] = strip(sys2varPipe(hmac, "openssl base64") )
for (k in headerParams)
header[j++] = urlencodeawk(k, "rawphp") "=" urlencodeawk(headerParams[k], "rawphp")
return sprintf("%s", "Authorization: OAuth " join(header, 0, length(header) - 1, ", "))
}
Java
Using ScribeJava:
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import com.github.scribejava.apis.MediaWikiApi;
import com.github.scribejava.core.builder.*;
import com.github.scribejava.core.model.*;
import com.github.scribejava.core.oauth.*;
public class MediaWikiOAuth1 {
private URL apiUrl = new URL("https://commons.wikimedia.org/w/api.php");
private String consumerToken = "";
private String consumerSecret = "";
private String accessToken = "";
private String accessSecret = "";
private String userAgent = ""; // https://meta.wikimedia.org/wiki/User-Agent_policy
private OAuth1AccessToken oAuthAccessToken;
private OAuth10aService oAuthService;
private String csrfToken;
public MediaWikiOAuth1() throws IOException {
oAuthService = new ServiceBuilder(consumerToken).apiSecret(consumerSecret).build(MediaWikiApi.instance());
oAuthAccessToken = new OAuth1AccessToken(accessToken, accessSecret);
// Check authentication
System.out.println(queryUserInfo());
// Fetch CSRF token, mandatory for upload using the MediaWiki API
System.out.println(queryTokens());
csrfToken = ""; // should be extracted from above JSON response
// Upload a file using the CSRF token
System.out.println(upload("wikicode", "filename.jpg", new URL("http://server/filename.jpg")));
}
public String queryTokens() throws IOException {
return apiHttpGet("?action=query&meta=tokens");
}
public String queryUserInfo() throws IOException {
return apiHttpGet("?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|ratelimits");
}
private String apiHttpGet(String path) throws IOException {
return httpGet(apiUrl.toExternalForm() + path + "&format=json");
}
private String apiHttpPost(Map<String, String> params) throws IOException {
return httpPost(apiUrl.toExternalForm(), params);
}
private String httpGet(String url) throws IOException {
return httpCall(Verb.GET, url, Collections.emptyMap(), Collections.emptyMap());
}
private String httpPost(String url, Map<String, String> params) throws IOException {
return httpCall(Verb.POST, url, Map.of("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"),
params);
}
private String httpCall(Verb verb, String url, Map<String, String> headers, Map<String, String> params)
throws IOException {
OAuthRequest request = new OAuthRequest(verb, url);
request.setCharset(StandardCharsets.UTF_8.name());
params.forEach(request::addParameter);
headers.forEach(request::addHeader);
request.addHeader("User-Agent", userAgent);
oAuthService.signRequest(oAuthAccessToken, request);
try {
return oAuthService.execute(request).getBody();
} catch (InterruptedException | ExecutionException e) {
throw new IOException(e);
}
}
public String upload(String wikiCode, String filename, URL url) throws IOException {
String apiResponse = apiHttpPost(Map.of(
"action", "upload",
"comment", "",
"format", "json",
"filename", filename,
"ignorewarnings", "1",
"text", wikiCode,
"token", csrfToken,
"url", url.toExternalForm()));
if (!apiResponse.contains("success")) {
throw new IllegalArgumentException(apiResponse);
}
return apiResponse;
}
}
Algorithm
Authorization: OAuth oauth_consumer_key="<consumer key>", oauth_token="<access token>", oauth_signature_method="HMAC-SHA1", oauth_signature="<base64(HMAC_SHA1(key="<consumer secret>&<token secret>", text="<signature base string>"))>", oauth_timestamp="<current time as unix timestamp>", oauth_nonce="<a long random string>", oauth_version="1.0"
where signature base string is the urlencoded, &-concatenated list of the request method, the request endpoint (ie. the full URL to api.php), and all the parameters of the request (GET, POST, and Authorization header, except oauth_signature itself) in lexicographic order.
For example, computing the header in PHP would look like this (cutting some corners such as nested parameter handling):
function oauthHeader( $consumerKey, $consumerSecret, $accessToken, $accessSecret, $method, $url, $data ) {
$headerParams = [
'oauth_consumer_key' => $consumerKey,
'oauth_token' => $accessToken,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_nonce' => md5( microtime() . mt_rand() ),
'oauth_version' => '1.0',
];
$allParams = array_merge( $headerParams, $data );
uksort( $allParams, 'strcmp' );
$allParamsJoined = array();
foreach ( $allParams as $key => $value ) {
$allParamsJoined[] = rawurlencode( $key ) . '=' . rawurlencode( $value );
}
$urlParts = parse_url( $url );
$url = $urlParts['scheme'] . '://' . strtolower( $urlParts['host'] ) . $urlParts['path'];
$signatureBaseParts = [ strtoupper( $method ), $url, implode( '&', $allParamsJoined ) ];
$signatureBaseString = implode( '&', array_map( 'rawurlencode', $signatureBaseParts ) );
$headerParams['oauth_signature'] = base64_encode( hash_hmac( 'sha1', $signatureBaseString, rawurlencode( $consumerSecret ) . '&' . rawurlencode( $accessSecret ), true ) );
$header = array();
foreach ( $headerParams as $key => $value ) {
$header[] = rawurlencode( $key ) . '=' . rawurlencode( $value );
}
return 'Authorization: OAuth ' . implode( ', ', $header );
}