Jump to content

OAuth/オーナー専用コンシューマー

From mediawiki.org
This page is a translated version of the page OAuth/Owner-only consumers and the translation is 23% complete.

オーナー専用コンシューマーは、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 );
}

関連項目