WP-API / node-wpapi

An isomorphic JavaScript client for the WordPress REST API
http://wp-api.org/node-wpapi/
MIT License
1.68k stars 191 forks source link

OAuth support #102

Open CaptainN opened 9 years ago

CaptainN commented 9 years ago

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.

kadamwhite commented 9 years ago

@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

mzalewski commented 8 years ago

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

kadamwhite commented 8 years ago

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

mzalewski commented 8 years ago

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

kadamwhite commented 8 years ago

:+1:

mzalewski commented 8 years ago

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

kadamwhite commented 8 years ago

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

kadamwhite commented 8 years ago

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!
mzalewski commented 8 years ago

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 commented 8 years ago

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)

kadamwhite commented 8 years ago

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 commented 8 years ago

@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.

mzalewski commented 8 years ago

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 commented 8 years ago

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

kadamwhite commented 8 years ago

@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.

mzalewski commented 8 years ago

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 notifications@github.com 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 commented 8 years ago

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 commented 8 years ago

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

justingreerbbi commented 8 years ago

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.

kadamwhite commented 8 years ago

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

kadamwhite commented 8 years ago

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 commented 8 years ago

Related reading:

mzalewski commented 8 years ago

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?

shiva-avula-nuk commented 7 years ago

@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.

mzalewski commented 7 years ago

@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.

shivakumaravula commented 7 years ago

@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 ?

kadamwhite commented 7 years ago

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!

d3v-null commented 7 years ago

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

mzalewski commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

Decided to try getting an implementation going anyway, basically combines the three-legged OAuth1.0a flow gist with the architecture/pattern used by the httpTransport functions

https://github.com/mzalewski/node-wpapi/tree/oauth-option-1

Comparison: https://github.com/WP-API/node-wpapi/compare/master...mzalewski:oauth-option-1

d3v-null commented 7 years ago

Thanks Matthew, I managed to get a totally working implementation of oauth 3 leg which automatically generates the access token if you don't have it. I just had some bug in the sorting algorithm somewhere :P

It's all up and ready to try out if you want to give it a go!

On 21 Oct. 2016, at 10:57, K.Adam White notifications@github.com wrote:

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!

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or mute the thread.

ludoo0d0a commented 7 years ago

Couldn't be possible to simply inject access token, if we used Passport as Express Middlware ? I'm connected to my WP instance with Passport+oauth2 strategy, I just need to inject token into wpapi now.

mzalewski commented 7 years ago

@ludoo0d0a Currently, your best (probably only) option is to write a new http transport (eg: https://github.com/mzalewski/wpapi-oauth-example-transport)

Bear in mind that the example above doesn't cover redirects (ie: auth is lost when redirected from users/me to users/1)

Hopefully we'll see some support built in soon, but I imagine things are pretty hectic with the 4.7 release coming up

Any code you manage to come up with would be helpful too :)

mzalewski commented 7 years ago

@derwentx Interesting approach, so it basically handles the auth step/submits the form for the user?

How does the user specify their username/password?

(Sorry, not that familiar with python :))

d3v-null commented 7 years ago

@mzalewski yep, that's right, it's the same way that you specify the consumer_token and consumer_secret. They are passed as arguments to the oauth constructor. If people actually end up using my client I'll probably find some way of storing the oauth token locally on disk so that it persists through multiple sessions and so that the password is only needed once, to generate the token, then while the token is valid it just re-uses the old one

ludoo0d0a commented 7 years ago

I tried to introduce oauth2 in @mzalewski http transport. For now access token is not kept/passed in the best way, but it still failed to authenticate with bearer token against my WP. Not clear what is wrong. Here is a related issue

    function _signOAuth( request, options ) {
        // If no oauth credentials, skip signing
        if ( ! oAuthCredentials || ! oAuthCredentials.accessToken )
            return request;
        var authHeader='';
        if (oAuthCredentials.version===2){
            var oauth2 = new oauth.OAuth2(oAuthCredentials.clientId,  oAuthCredentials.clientSecret,'', oAuthCredentials.authorizationURL, oAuthCredentials.tokenURL, oAuthCredentials.customHeaders);
            authHeader = oauth2.buildAuthHeader(oAuthCredentials.accessToken);
        }else{
            var oauth1 = new oauth.OAuth("", "", oAuthCredentials.clientId, oAuthCredentials.clientSecret, '1.0',null,'HMAC-SHA1' );
            authHeader = oauth1.authHeader(request.url, oAuthCredentials.accessToken, oAuthCredentials.tokenSecret, request.method);
        }

        request.set('Authorization', authHeader);
        return request;
    }
rmccue commented 7 years ago

If you're using the official OAuth plugin, note that it is OAuth 1.0a, not OAuth 2.0.

ludoo0d0a commented 7 years ago

Sure, but I use a real oauth2 : https://fr.wordpress.org/plugins/oauth2-provider/

andreasvirkus commented 7 years ago

Could anyone give some input in https://github.com/WP-API/node-wpapi/issues/312? @mzalewski You mentioned that getting the token is the hard part (which I've managed). How would I then go about signing the requests with the jwt?

velara3 commented 4 years ago

I'd like to use this library in a client application for making REST API calls to a WP instance (add, update, delete). To do this I need to use Basic authorization or OAuth in the client application to authorize REST API calls, correct? Seeing this issue is open what is the recommended path forward?