node-wpapi icon indicating copy to clipboard operation
node-wpapi copied to clipboard

OAuth support

Open CaptainN opened this issue 8 years ago • 41 comments

Hi!

What's involved with making oauth work (either oauth1, for which there is a WP API plugin plugin, or OAuth 2 for which there is a standalone plugin) with this plugin instead of standard HTTP authentication?

Kevin N.

CaptainN avatar Jul 21 '15 03:07 CaptainN

@CaptainN OAuth isn't currently supported by this library -- the basic process, if I understand it (and I'll be honest, I've never used OAuth successfully without confusion) is that you'd use the plugin to generate a key, then we'd need to add a facility to this API to make a request with that key to get the token used to authenticate.

It's definitely something I'd like to add, but I haven't had the time to get to it yet and adding media support (and more robustly supporting/testing against the WP-API 2.0 betas) are the priorities right now. If you've got OAuth experience, or interest, I'd love to get some help putting this in though

kadamwhite avatar Jul 21 '15 04:07 kadamwhite

I doubt anyone has used OAuth without confusion :) I've managed to get OAuth working though, so I should be able to help out

mzalewski avatar Oct 24 '15 00:10 mzalewski

@mzalewski if you're interested, that would be excellent in the extreme!

kadamwhite avatar Oct 24 '15 02:10 kadamwhite

Getting there slowly, I've got OAuth 1.0a working now. The only issue is that it requires user/browser authorization, so haven't quite figured out the best way to handle that. At the moment, I'm assuming that part will be done by the time the API is called so we already have an access token - but I have example code that does the actual authentication.

I'm planning to push it up tomorrow anyway so I'll let you know when it's up

mzalewski avatar Oct 28 '15 16:10 mzalewski

:+1:

kadamwhite avatar Oct 28 '15 16:10 kadamwhite

I've added OAuth 1.0A support to my fork here: https://github.com/mzalewski/wordpress-rest-api

I'm not 100% happy with it yet, but should hopefully give everyone a basic understanding of the direction I'm going. I've added some example code to the readme, so (hopefully) should be easy to understand if anyone wants to try it out

mzalewski avatar Oct 29 '15 10:10 mzalewski

Thanks, @mzalewski , I will check it out next week. I appreciate your sharing your efforts!

kadamwhite avatar Oct 29 '15 15:10 kadamwhite

I discussed the currently-supported OAuth flows with @rmccue today in slack; transcript below

  • rmccue [7:35 PM] @kadamwhite: Go ahead and ask :simple_smile:
  • kadamwhite [7:37 PM] @rmccue: :) Thanks. First off, is there any way, with the current oauth infrastructure, to have a completely server-side oauth process; i.e., without any need for the user to open a browser, assuming the app has previously had a key generated for it?
  • rmccue [7:38 PM] Using OAuth, not really [7:38] That's typically called "one-legged" OAuth, and the way it works is by having a pre-authorised key/secret [7:38] Technically possible with the plugin, but you'd need to write an addon plugin to add UI for it
  • kadamwhite [7:39 PM] hmm. ok. So there's not really any solution right now where a server-side app could authenticate with the wp-api, even if that app has previously been authorized and the user has entered their credentials into something accessible to the app — true? [7:39] I ask because the #1 request i've gotten for the wp-api node client was oauth, but it sounds like that isn't really possible and that's frustrating.
  • rmccue [7:42 PM] You could through an initial browser integration [7:42] If you have a look at the example client, that spits out the key/secret at the end of the process, so they could run that then just copy that across [7:43] But generally no, there's no server-server authentication right now [7:43] The #core-passwords team is working on "app passwords", which will help with that [7:44] http://oauth1.wp-api.org/docs/advanced/Desktop.html may also help
  • kadamwhite [7:44 PM] how long-lived are the tokens?
  • rmccue [7:47 PM](Sorry, was just making a :coffee:) [7:48] Currently, they do not expire [7:48] That might change in the future, but 1.0a doesn't have a "refresh" mechanism, so you'd need to re-auth from scratch [7:48] The plugin also currently doesn't recognise if a client has already been authorised, which might change
  • kadamwhite [7:52 PM] > so you'd need to re-auth from scratch in the event that you lose the token, you mean? given that it won't expire on its own. I'm also uncertain about the nuance of "the plugin doesn't currently recognize if a client has already been authorized"
  • rmccue [7:52 PM] If the plugin ​did​ expire tokens
  • kadamwhite [7:52 PM] ah, i see
  • rmccue [7:52 PM] Potentially could in the future, but the flow would suck
  • kadamwhite [7:52 PM] nods
  • rmccue [7:53 PM] And basically, when you send off an authorisation request to some OAuth providers, they'll tell you if you've already been given a token/secret for the user, and just give that back to you [7:53] The plugin currently doesn't do that, so you could end up with multiple tokens per user for the same app
  • kadamwhite [7:54 PM] if they request more than one, you mean?
  • rmccue [7:54 PM] That'll probably change, since there's no reason to do that
  • kadamwhite [7:54 PM] if you re-use a token it should work?
  • rmccue [7:54 PM] Yep [7:54] Some client apps do the lazy thing and don't store user state [7:54] They just reauthorise every time
  • kadamwhite [7:55 PM] So, to recap: the best-case scenario for node-driven applications that wish to auth via oauth 1.0a would be,
    1. User creates an app secret etc using the oauth plugin's wp-admin screen under the "users" menu
    2. App requests authentication; user is required to open a browser in some capacity in order to enter their credentials, there is no way for any purely server-side logic to inject those into the process without user interaction
    3. Browser flow could end by displaying a token to the user that they can copy into their app's configuration file
    4. App could henceforth use that token to auth directly, just for that user missing anything?
  • rmccue [7:56 PM] Nope, sounds right to me
  • kadamwhite [7:56 PM] I'm glad to hear there's a group working on it because that's less than ideal, but I get how we're in this situation. Thanks for helping walk me through it.
  • rmccue [7:56 PM] However, you ​could​ build a small plugin that essentially does 1-3 on the server
  • kadamwhite [7:56 PM] howso…? thought you'd said that wasn't an option
  • rmccue [7:56 PM] Essentially, it'd create the token and authorise it automatically [7:57] https://wordpress.slack.com/archives/core-restapi/p1453250332004937
  • kadamwhite [7:57 PM] hmm, which server are we talking about, wp's or the app's?
  • rmccue [7:57 PM] WP's
  • kadamwhite [7:57 PM] ahh I misunderstood
  • rmccue [7:57 PM] :simple_smile: [7:57] Yeah, should have said "provider" instead
  • kadamwhite [7:57 PM] I'll probably want to pick your brain about that as well, then, because that would be of major benefit
  • rmccue [7:57 PM] Indeed
  • kadamwhite [7:58 PM] for now, we're ostensibly waiting for company to come over (I have my doubts), so I need to run. Thanks for your time, Ryan!

kadamwhite avatar Jan 20 '16 01:01 kadamwhite

That server-side plugin sounds interesting, would you consider that?

oAuth 2.0 appears offer a few more options though - combined with the oAuth 2 plugin on WP.org (https://wp-oauth.com/knowledge-base/grant-types/), it might be possible to connect via oAuth without requiring a redirect.

mzalewski avatar Jan 20 '16 03:01 mzalewski

What about simply exposing a way to hook into the Auth function and passing off authentication to third-party code/modules?

It could be the easiest option given the number of possible uses of this plugin (eg: web-based, desktop, library)

mzalewski avatar Jan 20 '16 03:01 mzalewski

Oauth support's the most common question we've gotten, so ideally I'd like to have something set up within the package itself. Lots of ways to move toward that, though! I finally reviewed the implementation you linked to, it's interesting -- could you walk me through the process of using it within your application's code?

kadamwhite avatar Jan 20 '16 06:01 kadamwhite

@mzalewski forgot to respond to the other part of your comment: I would 100% be interested in an add-on to provide https://wp-oauth.com/ compatibility for this client library, but I'm hesitant to implement support for a non-WP-native, paid plugin into the core wordpress-rest-api package. I feel it would be better to hold out for the core-passwords team's app passwords, or to investigate writing a companion WP plugin to extend the 1.0a server to give access to the node client in the way discussed in the chat above.

kadamwhite avatar Jan 21 '16 06:01 kadamwhite

Fair enough - it definitely makes sense to support the official WP authentication methods out of the box.

I'm happy to walk through my fork, but it's been a pretty busy week - I'll free up some time to go through it properly within the next 1-2 days

I'll give a basic overview (from memory): I've put most of the oAuth handling code into the oAuth1.js file, with some changes to WP-Request.js which passes URL signing off to the oAuth object.

Both Client ID and Client Secret are required parameters and are configured in WP. These, along with the oAuth URLs provided by the API, are used to construct an OAuth object ( new WP.OAuth )

oauth.getAuthorizeUrl

Sends a request WP API and receives a "request" token which is used to sign the Authorize URL (URL provided to promise callback).

User Redirect

The User is then required to log in on the site at the provided URL

oauth.getAccessToken

The getAccessToken function can then be used by passing the OAuth Token and verification code. This function is responsible for sending the access/verify tokens to the server, and finally receiving an authenticated token.

Once we have that authenticated token, it is stored for future use and will be used to authenticate outgoing requests.

It uses the OAuth Node (node-oauth) library behind the scenes, so the documentation there may help - the code I've written simply wraps node-oauth and attempts to integrate it into your functions.

Hopefully that helps understand what I've done.

I also came across this while working on it: http://passportjs.org/ - It doesn't solve any of the issues around redirecting users, but from what I can tell it is a popular library for working with oAuth and could possibly save a lot of time?

mzalewski avatar Jan 21 '16 06:01 mzalewski

Sorry if it's confusing, even I am having trouble reading what I've just written. I'll see if I can put together a diagram which should explain things more clearly

mzalewski avatar Jan 21 '16 06:01 mzalewski

@mzalewski no problem whatsoever, I appreciate it. I'm going to have some concerted time to work on this in conjunction with the WP-API team at the feelingrestful.com event next week, so if you get a chance to go through it in more detail by Wednesday or so that'd be very helpful; but as time permits this weekend or on the flight I'll take what you've given me already and see what I can make of it!

Thanks a ton.

kadamwhite avatar Jan 21 '16 06:01 kadamwhite

Ok great - looks like it'll be a great event :) I'll definitely put some time aside to go through it before then.

On Thu, Jan 21, 2016 at 4:51 PM, K.Adam White [email protected] wrote:

@mzalewski https://github.com/mzalewski no problem whatsoever, I appreciate it. I'm going to have some concerted time to work on this in conjunction with the WP-API team at the feelingrestful.com event next week, so if you get a chance to go through it in more detail by Wednesday or so that'd be very helpful; but as time permits this weekend or on the flight I'll take what you've given me already and see what I can make of it!

Thanks a ton.

— Reply to this email directly or view it on GitHub https://github.com/kadamwhite/wordpress-rest-api/issues/102#issuecomment-173476285 .

mzalewski avatar Jan 21 '16 07:01 mzalewski

Sorry - hope I'm not too late.. Here is a basic overview of the process (found on another site).

The dashed lines represent server-to-server communication, and the solid lines are user/browser redirects.

I'll go through the steps in the image and try to match it up with the code I added. Step A: This is the oauth1.getAuthorizeUrl call - it uses the client ID and secret to request a "Request Token". The second part of this function then generates a URL for the user to visit.

Step B: The user must navigate to the signed URL generated in Step A - either by redirect, or manual browser navigation.

Step C: The server (WP) will verify the Request token and generate a verification Token

Step D: Now we have a verification Token, it needs to be passed back to the Node App - either by redirect, or by asking the user to copy+paste it.

Step E: oauth1.getAccessToken will use the verification token to ask the server for an Access Token.

Step F+: Access Token is granted by the server and is used to sign all future requests.

Overview

mzalewski avatar Jan 27 '16 12:01 mzalewski

I've also updated my repository with 2 examples (examples folder) - one is an express app which will redirect the user to WP to obtain authorization, the other asks the user to copy and paste URLs/tokens via the console

mzalewski avatar Jan 27 '16 12:01 mzalewski

I wanted to stop by and put in 2 cents! I am going to be putting a full strip down native WP version of WP OAuth Server which will be a bare bones server with no license or paid version. DB Structure will be merged to handle consumers just as the OAuth1.0a plugin does.

I am open for helping out where I can.

justingreerbbi avatar Jan 28 '16 15:01 justingreerbbi

Tagging https://github.com/joehoyle/wordpress-rest-api-oauth-1 as a potential source of inspiration or code for this situation

kadamwhite avatar Jul 25 '16 16:07 kadamwhite

Picked this back up today and wrote this script, which (if combined with the fix in WP-API/OAuth1#155) will follow the out-of-band three-legged OAuth1.0a flow to get the verification code needed to authorize requests. It is the starting point for further work to actually sign the requests within WPRequest.

A challenge for OAuth handling is going to be how we can design the transport/library seam to make the code library-independent; that may be out of scope for 1.0 however.

'use strict';

var opn = require( 'opn' );
var prompt = require('prompt');

var OAuth = require( 'oauth' );
var oauth = new OAuth.OAuth(
    // reqURL
    'http://wpapi.loc/oauth1/request',
    // accessURL
    'http://wpapi.loc/oauth1/access',
    // Key
    'OL5EIwSTQyPr',
    // Secret
    'YDBGBezQPDd51DwDIDhBfrYeSOUJqCQwcHwRnVYebGAmFtU1',
    // Version
    '1.0A',
    // authorize_callback (null in example)
    'oob',
    // Signature method
    'HMAC-SHA1'
    // nonceSize
    // customHeaders
);

// console.log( auth );

function getRequestToken() {
    return new Promise( ( resolve, reject ) => {
        oauth.getOAuthRequestToken(function( err, token, secret, results ) {
            if ( err ) {
                return reject( err );
            }
            console.log( results );
            resolve({
                token: token,
                secret: secret
            });
        });
    });
}
function getAccessToken(config) {
    return new Promise( ( resolve, reject ) => {
        oauth.getOAuthAccessToken( config.token, config.secret, config.verifier, function( err, token, secret, results ) {
            if ( err ) {
                return reject( err );
            }
            resolve({
                token: token,
                secret: secret
            });
        });
    });
}

getRequestToken()
    .then(function(config) {
        opn(`http://wpapi.loc/oauth1/authorize?oauth_token=${config.token}&oauth_callback=oob`);
        prompt.start();
        return new Promise( ( resolve, reject ) => {
            prompt.get([
                'verifier'
            ], ( err, result ) => {
                if ( err ) {
                    return reject( err );
                }
                resolve({
                    token: config.token,
                    secret: config.secret,
                    verifier: result.verifier
                });
            });
        });
    })
    .then(function( config ) {
        console.log( config );
        return getAccessToken( config );
    })
    .then(function( result ) {
        console.log( result );
    })
    .catch( err => console.error( err ) );

kadamwhite avatar Aug 18 '16 02:08 kadamwhite

Related reading:

  • Unclear why ~~this gist~~ (deleted) is failing (code based on Joe Hoyle's library); I've requested his review, time permitting
    • Update: the issue was a typo'd secret, all three of these scripts properly achieve the first leg of the three-legged flow: gist
  • https://www.npmjs.com/package/oauth-1.0a#client-side-usage-caution
  • OAuth plugin docs around credentials acquisition
  • More in-depth guide to the three-legged flow

kadamwhite avatar Aug 18 '16 15:08 kadamwhite

Great - If we can handle leg 1/3 without user interaction, is making leg 2 pluggable an option, defaulting to OOB/prompt?

eg: https://gist.github.com/mzalewski/2b2ff2de0d88a5c3194c407b796695df

What are the main issues you're having?

A challenge for OAuth handling is going to be how we can design the transport/library seam to make the code library-independent; that may be out of scope for 1.0 however.

Unless I misunderstood what you meant, wouldn't it be easier to just focus on superagent since it's used by the rest of the library anyway (assuming the library will be handling leg1/3 behind the scenes). It isn't required in the second leg (where the user will need to perform the authorization) so shouldn't make any difference to users?

mzalewski avatar Aug 18 '16 23:08 mzalewski

@kadamwhite @mzalewski This is exactly what I needed. We have moved from basic to oauth on our Wordpress domain. Please help me out in implementing the above.

shiva-avula-nuk avatar Oct 19 '16 09:10 shiva-avula-nuk

@shiva-avula-nuk Basically, what you need is an Access Token. Once you have that, you can sign requests via the Auth HTTP header.

The main issue is: to get that OAuth Access token, there 3 legs/processes we have to go through and the second one requires user interaction (from a user that has authorized access to WP). As this is a UI/platform agnostic JS library, it's (almost?) impossible to handle that 2nd leg in a way that makes sense in all scenarios.

Start with the code that @kadamwhite posted: https://github.com/WP-API/node-wpapi/issues/102#issuecomment-240609409 - that'll get you the access token. Then you can start signing requests - I've been meaning to get back into WP API stuff again, so I'll post a blog post over the weekend that should hopefully give some ideas.

mzalewski avatar Oct 20 '16 06:10 mzalewski

@mzalewski Looked at the code, but did not execute yet. However I already have all the credentials required for Wordpress API, all I need is to make WP to use those OAuth credentials in making API call to Wordpress API. What is the easiest way to implement this ?

shivakumaravula avatar Oct 20 '16 07:10 shivakumaravula

Apologies for the delay on this, been pretty heads-down on working on the underlying API itself—as we come up for air this is the next priority.

One open question is what interface is most useful for navigating through the initial three-legged handshake, from supplying the initial token and secret, to getting the URL (to open or to redirect to), to then provide a path to capture the returned values from the callback or the OOB flow. If you have methods or flows you are using that are working for you, please share them!

kadamwhite avatar Oct 20 '16 23:10 kadamwhite

Hey, I've been following this thread for the suggestions people have been making. I've actually been working on my own Python OAuth1a 3leg flow client, and it works! (sort of) I've managed to succesfully bypass getting the user to type their password in by using a scraper to fill in the form automatically, and I can succesfully grab access tokens from the site and authenticate requests! Unfortunately there's something up with Wordpress' implementation of the server and how it expects you to sign query parameters which is different from the spec I'm using (RFC 5849) so query parameters like ?page=2 don't work with authenticated requests yet but i'm working on a fix.

Check out the OAuth_3Leg class in oauth.py to see how I did the automated user authentication and let me know what you think! https://github.com/derwentx/wp-api-python

d3v-null avatar Oct 24 '16 00:10 d3v-null

@derwentx Let me know if you solve the query parameter thing. Does adding "?context=edit" work?

@Script-Shiva - I've got an implementation that seems to work ok here (though I've only done very basic testing). https://github.com/mzalewski/wpapi-oauth-example-transport

It completely overrides the http transport. Unfortunately I couldn't find another way to do it (without modifying node-wpapi source). So, basically a hacky workaround until OAuth support is added to the node-wpapi library.

It also only does the signing of requests - it assumes that the access token has already been generated.

mzalewski avatar Oct 24 '16 22:10 mzalewski

@kadamwhite Here are a few examples of some possible interfaces/workflows. The first requires OAuth-specific methods to be built into node-wpapi (which I think was your preferred option?) The other 2 simply allow an authentication handler to be injected, but ends up being slightly cleaner since node-wpapi doesn't need to worry about the verification.

I think the best option might be a combination of 1 and 3 (ie: built-in, but allow overriding via oauthHandler) - I'm happy to get some basic implementation going in a fork, but figured I should get an idea of the direction you want to go first.

https://gist.github.com/mzalewski/eaccd40e048102712af76dacac742415

mzalewski avatar Oct 24 '16 23:10 mzalewski