CSteipp has uploaded a new change for review.
https://gerrit.wikimedia.org/r/93859
Change subject: Add OAuth identify method
......................................................................
Add OAuth identify method
Allow OAuth Consumers to get a signed JWT about the user who authorized
the Consumer.
Change-Id: I86c0031a4a479994eb38aa7873ac665054d75d17
---
M OAuth.setup.php
M frontend/specialpages/SpecialMWOAuth.php
A lib/JWT.php
3 files changed, 278 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/OAuth
refs/changes/59/93859/1
diff --git a/OAuth.setup.php b/OAuth.setup.php
index 4db51a9..5333b15 100644
--- a/OAuth.setup.php
+++ b/OAuth.setup.php
@@ -76,6 +76,7 @@
$classes['OAuthServer'] = "$libDir/OAuth.php";
$classes['OAuthDataStore'] = "$libDir/OAuth.php";
$classes['OAuthUtil'] = "$libDir/OAuth.php";
+ $classes['JWT'] = "$libDir/JWT.php";
# Storage
$classes['MWOAuthDataStore'] =
"$backendDir/MWOAuthDataStore.php";
diff --git a/frontend/specialpages/SpecialMWOAuth.php
b/frontend/specialpages/SpecialMWOAuth.php
index 34bef85..f07aa67 100644
--- a/frontend/specialpages/SpecialMWOAuth.php
+++ b/frontend/specialpages/SpecialMWOAuth.php
@@ -121,6 +121,33 @@
$format
);
break;
+ case 'identify':
+ wfDebugLog( 'OAuth', __METHOD__ . ":
Doing Identify" );
+ $format = 'json'; // we only return
JWT, so we assume json
+ $server =
MWOAuthUtils::newMWOAuthServer();
+ $oauthRequest =
MWOAuthRequest::fromRequest( $request );
+ // verify_request throws an exception
if anything isn't verified
+ list( $consumer, $token ) =
$server->verify_request( $oauthRequest );
+
+ $wiki = wfWikiID();
+ $dbr = MWOAuthUtils::getCentralDB(
DB_SLAVE );
+ $access =
MWOAuthConsumerAcceptance::newFromToken( $dbr, $token->key );
+ // Access token is for this wiki
+ if ( $access->get( 'wiki' ) !== '*' &&
$access->get( 'wiki' ) !== $wiki ) {
+ throw new MWOAuthException(
+
'mwoauth-invalid-authorization-wrong-wiki',
+ array( $wiki )
+ );
+ }
+ $localUser =
MWOAuthUtils::getLocalUserFromCentralId( $access->get( 'userId' ) );
+ if ( !$localUser ||
!$localUser->isLoggedIn() ) {
+ throw new MWOAuthException(
'mwoauth-invalid-authorization-invalid-user' );
+ }
+
+ // We know the identity of the user who
granted the authorization
+ wfDebugLog( 'OAuth', __METHOD__ . ":
Sending out JWT" );
+ $this->outputJWT( $localUser,
$consumer, $oauthRequest, $format );
+ break;
default:
$format = $request->getVal( 'format',
'html' );
$this->showError(
'mwoauth-bad-request', $format );
@@ -136,6 +163,48 @@
$this->getOutput()->addModuleStyles( 'ext.MWOAuth.BasicStyles'
);
}
+ /**
+ * Make statements about the user, and sign the json with
+ * a key shared with the Consumer.
+ * @param User $user the user who is the subject of this request
+ * @param OAuthConsumer $consumer
+ */
+ protected function outputJWT( $user, $consumer, $request, $format ) {
+ global $wgCanonicalServer;
+ $statement = array();
+
+ // Include some of the OpenID Connect attributes
+ // http://openid.net/specs/openid-connect-core-1_0.html (draft
14)
+ // Issuer Identifier for the Issuer of the response.
+ $statement['iss'] = $wgCanonicalServer;
+ // Subject identifier. A locally unique and never reassigned
identifier.
+ $statement['sub'] = MWOAuthUtils::getCentralIdFromLocalUser(
$user );
+ // Audience(s) that this ID Token is intended for.
+ $statement['aud'] = $consumer->key;
+ // Expiration time on or after which the ID Token MUST NOT be
accepted for processing.
+ $statement['exp'] = wfTimestamp() + 100;
+ // Time at which the JWT was issued.
+ $statement['iat'] = wfTimestamp();
+ // String value used to associate a Client session with an ID
Token, and to mitigate
+ // replay attacks. The value is passed through unmodified from
the Authorization Request.
+ $statement['nonce'] = $request->get_parameter( 'oauth_nonce' );
+ // TODO: Add auth_time, if we start tracking last login
timestamp
+
+ // Include some MediaWiki info about the user
+ if ( !$user->isHidden() ) {
+ $statement['username'] = $user->getName();
+ $statement['editcount'] = intval( $user->getEditCount()
);
+ $statement['confirmed_email'] =
$user->isEmailConfirmed();
+ $statement['blocked'] = $user->isBlocked();
+ $statement['registered'] = $user->getRegistration();
+ $statement['groups'] = $user->getEffectiveGroups();
+ $statement['rights'] = array_values( array_unique(
$user->getRights() ) );
+ }
+
+ $JWT = JWT::encode( $statement, $consumer->secret );
+ $this->showResponse( $JWT, $format );
+ }
+
protected function handleAuthorizationForm( $requestToken, $consumerKey
) {
$this->getOutput()->addSubtitle( $this->msg( 'mwoauth-desc'
)->escaped() );
diff --git a/lib/JWT.php b/lib/JWT.php
new file mode 100644
index 0000000..7a7b4a0
--- /dev/null
+++ b/lib/JWT.php
@@ -0,0 +1,208 @@
+<?php
+
+/**
+ * JSON Web Token implementation, based on this spec:
+ * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
+ *
+ * PHP version 5
+ *
+ * @category Authentication
+ * @package Authentication_JWT
+ * @author Neuman Vong <[email protected]>
+ * @author Anant Narayanan <[email protected]>
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+/**
+ * JSON Web Token implementation, based on this spec:
+ * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
+ *
+ * @category Authentication
+ * @package Authentication_JWT
+ * @author Neuman Vong <[email protected]>
+ * @author Anant Narayanan <[email protected]>
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+class JWT
+{
+ /**
+ * Decodes a JWT string into a PHP object.
+ *
+ * @param string $jwt The JWT
+ * @param string|null $key The secret key
+ * @param bool $verify Don't skip verification process
+ *
+ * @return object The JWT's payload as a PHP object
+ * @throws UnexpectedValueException Provided JWT was invalid
+ * @throws DomainException Algorithm was not provided
+ *
+ * @uses jsonDecode
+ * @uses urlsafeB64Decode
+ */
+ public static function decode($jwt, $key = null, $verify = true)
+ {
+ $tks = explode('.', $jwt);
+ if (count($tks) != 3) {
+ throw new UnexpectedValueException('Wrong number of segments');
+ }
+ list($headb64, $bodyb64, $cryptob64) = $tks;
+ if (null === ($header =
JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) {
+ throw new UnexpectedValueException('Invalid segment encoding');
+ }
+ if (null === $payload =
JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) {
+ throw new UnexpectedValueException('Invalid segment encoding');
+ }
+ $sig = JWT::urlsafeB64Decode($cryptob64);
+ if ($verify) {
+ if (empty($header->alg)) {
+ throw new DomainException('Empty algorithm');
+ }
+ if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) {
+ throw new UnexpectedValueException('Signature verification
failed');
+ }
+ }
+ return $payload;
+ }
+
+ /**
+ * Converts and signs a PHP object or array into a JWT string.
+ *
+ * @param object|array $payload PHP object or array
+ * @param string $key The secret key
+ * @param string $algo The signing algorithm. Supported
+ * algorithms are 'HS256', 'HS384' and 'HS512'
+ *
+ * @return string A signed JWT
+ * @uses jsonEncode
+ * @uses urlsafeB64Encode
+ */
+ public static function encode($payload, $key, $algo = 'HS256')
+ {
+ $header = array('typ' => 'JWT', 'alg' => $algo);
+
+ $segments = array();
+ $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
+ $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
+ $signing_input = implode('.', $segments);
+
+ $signature = JWT::sign($signing_input, $key, $algo);
+ $segments[] = JWT::urlsafeB64Encode($signature);
+
+ return implode('.', $segments);
+ }
+
+ /**
+ * Sign a string with a given key and algorithm.
+ *
+ * @param string $msg The message to sign
+ * @param string $key The secret key
+ * @param string $method The signing algorithm. Supported
+ * algorithms are 'HS256', 'HS384' and 'HS512'
+ *
+ * @return string An encrypted message
+ * @throws DomainException Unsupported algorithm was specified
+ */
+ public static function sign($msg, $key, $method = 'HS256')
+ {
+ $methods = array(
+ 'HS256' => 'sha256',
+ 'HS384' => 'sha384',
+ 'HS512' => 'sha512',
+ );
+ if (empty($methods[$method])) {
+ throw new DomainException('Algorithm not supported');
+ }
+ return hash_hmac($methods[$method], $msg, $key, true);
+ }
+
+ /**
+ * Decode a JSON string into a PHP object.
+ *
+ * @param string $input JSON string
+ *
+ * @return object Object representation of JSON string
+ * @throws DomainException Provided string was invalid JSON
+ */
+ public static function jsonDecode($input)
+ {
+ $obj = json_decode($input);
+ if (function_exists('json_last_error') && $errno = json_last_error()) {
+ JWT::_handleJsonError($errno);
+ } else if ($obj === null && $input !== 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $obj;
+ }
+
+ /**
+ * Encode a PHP object into a JSON string.
+ *
+ * @param object|array $input A PHP object or array
+ *
+ * @return string JSON representation of the PHP object or array
+ * @throws DomainException Provided object could not be encoded to valid
JSON
+ */
+ public static function jsonEncode($input)
+ {
+ $json = json_encode($input);
+ if (function_exists('json_last_error') && $errno = json_last_error()) {
+ JWT::_handleJsonError($errno);
+ } else if ($json === 'null' && $input !== null) {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $json;
+ }
+
+ /**
+ * Decode a string with URL-safe Base64.
+ *
+ * @param string $input A Base64 encoded string
+ *
+ * @return string A decoded string
+ */
+ public static function urlsafeB64Decode($input)
+ {
+ $remainder = strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= str_repeat('=', $padlen);
+ }
+ return base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ /**
+ * Encode a string with URL-safe Base64.
+ *
+ * @param string $input The string you want encoded
+ *
+ * @return string The base64 encode of what you passed in
+ */
+ public static function urlsafeB64Encode($input)
+ {
+ return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
+ }
+
+ /**
+ * Helper method to create a JSON error.
+ *
+ * @param int $errno An error number from json_last_error()
+ *
+ * @return void
+ */
+ private static function _handleJsonError($errno)
+ {
+ $messages = array(
+ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+ JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+ JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
+ );
+ throw new DomainException(
+ isset($messages[$errno])
+ ? $messages[$errno]
+ : 'Unknown JSON error: ' . $errno
+ );
+ }
+
+}
+
--
To view, visit https://gerrit.wikimedia.org/r/93859
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I86c0031a4a479994eb38aa7873ac665054d75d17
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/OAuth
Gerrit-Branch: master
Gerrit-Owner: CSteipp <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits