Apparently the way to get content from a Drupal site to another site/application is to use services. And also it would seem that using session based authentication is sooo last Drupal 6. So the recommended, yet not at all properly documented (like, anywhere), method is for an application to use 2-Legged OAuth authentication to authenticate and exchange data.
I spent some time getting angry at Drupal and OAuth and module makers and everyone who wouldn’t provide a clear guide how to do this, and so now I have what I believe is a correctly working implementation (if it’s not, please tell me where I’m wrong) I pass this information on to you dear reader.
The idea
Note: This is a copy of the same post I made on the Drupal site. Providing here as well as there as I think it’s important information.
So, the concept is this.
- You have a collection of nodes within Drupal you want an application to have access to. And you may want there to be a number of applications, each having access to a different set of nodes.
- You create within Drupal a user who won’t necessarily ever ‘login’ to the site, but becomes the namesake ‘consumer’ of these nodes. You give access to the nodes you want your application to see to this user
- This user becomes an ‘OAuth Consumer’ and gets a key and a secret created for them.
- Drupal knows the key and the secret, your application knows the key and the secret, but you only ever transmit the key. The secret is used at the client end to make a hash of the request string which the Drupal OAuth side compares to a hash it makes of the request using its secret. If the hashes match then both ends know the same secret and trust each other. (awwww)
- There’s a bit more in terms of requests and tokens and such, which we’ll get to below.
- With Drupal trusting the client, and knowing the client as the consumer as set up previously, it allows it to access all content that Drupal user has access to. This is done through requests via a service endpoint much like requesting content directly from drupal. (node request from Drupal = drupalsite/node/1, from endpoint drupalsite/endpoint/node/1)
Drupal Setup
NOTE: I initially tried doing all of this with a Drupal install with PostgreSQL as the db. Things did not go well. As much as it’s supposed to be abstracted away from the db, it really isn’t yet. So stick with mySQL if you can for least amount of pain.
I’ll try to recount the steps needed to get all of this working, but I went down a lot of paths before coming to what I think is the right one, so I may get things wrong.
Required Modules
I’m naming Modules from my list of modules that I have enabled and know I need for this to work.
- Content Access & ACL: You don’t necessarily needs these, unless you are restricting access to groups of nodes to particular users/roles. I did need this ability, so I will detail how to use these to allow a holder of a given access key/secret pair to get access to only the nodes you want them to
- Chaos Tools: Services requires this, as does OAuth Authentication
- OAuth: Very important module, and you need Oauth and OAuth Provider UI enabled. Youmust use the dev release of OAuth for 2-Legged OAuth to work (at time of writing)
- Services: Kind of important seeing as we’re making a Service. Again, use the dev release (at time of writing)
- OAuth Authentication: Appears by way of having Services and OAuth installed. Enable it.
- REST Server: Appears by way of having Services installed. Enable it.
SETUP
- Go to admin/config/services/oauth and Enable the oauth provider, Save configuration.
- Add a Context (admin/config/services/oauth/add) Give it a name,limit its signature methods to HMAC-SHA1 only. (Not sure why this is important to do, but apparently it is… so just do it). Create an Authorization Level and make it Selected by default. For 2-Legged, none of the fields for what appears on an authorization page matter because you’ll never see that page.
- Go to admin/people and add a user who will be an OAuth Consumer. Name, email and password are unimportant for OAuth, so name them as you wish.
- From within your user page, you should have an Authorization panel. Go to this, and then Add consumer. This is where you are making this user be a consumer of OAuth provided content. Multiple consumers can be assigned to a single user if need be. Give the consumer a name and a callback URL. The Callback URL is where the OAuth process would return to after authorizing a user. In the 2-Legged process we’re doing here it’s not used, but if using 3-Legged it would be the address of your client which can accept the authorized token and continue on with it. Select the Context you created earlier
- Go to admin/structure/services/add to add a resource endpoint. The Machine-readable name you give here will be a name for it to be referred to internally, and thePath to endpoint is the portion of the URL you enter after your base Drupal URL ( drupalsite.com/endpointname ). Set the Server to REST and the Authentication toOAuth authentication. (And turn on Debug if you like). Save this.
- Now in the list of Services you should see your new endpoint, click on Edit Resources for it. Within here is where you can allow and deny access to various portions of Drupal via this service (this is not where we limit which nodes people have access to, but rather whether they can access nodes at all. Select as you see fit, but for the sake of this example, at least node->retrieve has to be selected. Save your changes on this panel
- Still within the Services Endpoint screen for your endpoint, go to Authentication and select your Context, the default (only) Authorization level you created within that, and the Default required authentication to be Consumer key, also known as 2-legged OAuth. Save the endpoint.
Now we have an OAuth Context, with a Consumer (Authorized accessor) for that context. We have an endpoint with a scope of access of at least nodes and it will allow OAuth Consumers who have access to our created context. Now you need some content to access with the correct permissions. If you’re giving access to all content on your site, you can skip the portions about access control lists and per content access and just do things at a high level.
Content permissions
- admin/people/permissions will give you a listing of all permissions on various portions of Drupal, and also a sub-tab of Roles, if you’re limiting access to your consumer to only some nodes, go to Roles and create a new one. Now under Node (within Permissions), make sure that View published content is not ticked for Anonymous User, but only for Authenticated User (Which will inherit down to your newly created role too)
- Go to admin/structure/types/manage/ and select the content types you are going to be viewing (Basic Page, Book Page etc.) and within each of them go to Access Control, then tick Enable per content node access control settings and submit.
- What this means is that you can assign a role to the user you have made an OAuth Consumer, and you can then on a per node basis give them access to them or not. As said before, if you don’t need this level of control, you can skip these parts.
You should now have Drupal set up to be accepting requests. There are two php fixes within services/oauth to make things work correctly though.
Code changes
- When using any of the OAuth Services pages it’ll keep creating errors (which display on screen if you have debug on while doing things). This can be fixed by editing sites/all/modules/services/auth/services_oauth/services_oauth.inc at line 170 to read
if (is_array($settings) <strong>&& isset($settings['oauth_context'])</strong>) {
- This next change is a bit of a hack, and works for my case where I’m only ever using 2-Legged OAuth, and there may be a way to get this working properly, but I didn’t find it. The problem is, when running 2-Legged OAuth, when you request a token it should come back pre-authorized because you don’t do the authorization leg. I could not for the life of me work out how to make it return one with authorized = 1. So, I hacked the relevant file to default to having tokens authorized. This is bad for 3-Legged OAuth as it means you could skip the Authorize leg and get access to things without the user’s permission. So don’t do this if you want to use full OAuth. If you just want to use it like me though, and until I’m told how this should work properly, go tosites/all/modules/oauth/includes/DrupalAuthToken.inc and at the top of the file, where the variable definitions are, change
$authorized = 0
to$authorized = 1
You can test this by going to drupalsite.com/endpointpath/node/1 and see if you get a response. You should get
The request must be signed
Client Setup
So, this is where I found an awful lot of documentation really lacking. It took me forever to actually understand what all the bits were in an OAuth setup, and what each should be doing. Our server-side code is usually done in Java, so my example here is written as a Java Servlet. I ended up using the Scribe Java library as once you get used to it, it’s really easy to do OAuth authentication and calls with it. But even though it has a gazillion examples, they all just run as something that outputs data to standard out and require manually going to a web address and taking a token and pasting it back to standard in… it’s all very clunky and doesn’t show a real work example at all. I was confused as how you were supposed to take these not-real-world examples and translate them to something you would actually usein a setup.
I now have something that works, I could be doing things the hard way (I’m no Java coder), I could be doing things the wrong way, but I DO have a page I can go to that then, unbeknownst to the end user, goes and authenticates with the Drupal OAuth Server and requests the content.
Let’s see how.
Set up your Java Servlet
I won’t go into great detail here, as everyone has their different ways of doing things. But I’m using Eclipse and the Java EE perspective. I created a new ‘Dynamic Web Project’ within that, which gives me the basics of a servlet I can point a browser at. We use Glassfish as our server, and again, I won’t go into how you get that up and running, but once you have a Glassfish server running (locally usually) from within your eclipse, you can then add your new Dynamic Web Project to it and reference it in your browser (for examplehttp://localhost:8080/oauthExample/).
I copied the scribe source files into my Java Resources src folder within my project, but you could instead have scribe as a compiled jar and add it to your lib folder withinWebContent/WEB-INF.
Now I had a few classes to make. Firstly Scribe uses API Classes to define things about your particular OAuth setup. So I created a DrupalAPI.java file:
import org.scribe.builder.api.DefaultApi10a; import org.scribe.model.OAuthConfig; import org.scribe.model.Token; import org.scribe.oauth.OAuth10aServiceImpl; import org.scribe.oauth.OAuthService; public class DrupalApi extends DefaultApi10a { private static final String AUTHORIZATION_URL = "http://YOURDRUPALURL/oauth/authorize?oauth_token=%s"; private static final String BASE_URL = "http://YOURDRUPALURL/oauth/"; @Override public String getRequestTokenEndpoint() { return BASE_URL + "request_token"; } @Override public String getAccessTokenEndpoint() { return BASE_URL + "access_token"; } @Override public String getAuthorizationUrl(Token requestToken) { return String.format(AUTHORIZATION_URL, requestToken.getToken()); } @Override public OAuthService createService(OAuthConfig config) { return new OAuth10aServiceImpl(this, config); } }
For the 2-Legged process the AUTHORIZATION_URL isn’t needed, but it’s here for completeness.
Now for the bit that I never found examples for, the Servlet that handles things. This one doesn’t really handle requests and the content in any way, but will demonstrate it authenticating and fetching content from your drupal server. Also, there’s stuff in here that’s a hang over from the examples I copied, which don’t really do anything, I’m not finished with this servlet, but wanted to document a working example so people can actually DO something with Drupal, OAuth and Java if they want to. You’ll see mention of Help Pages through this class, that’s because I’m working on a system that serves help pages to an application from Drupal.
/** * Servlet implementation class DrupalOauthServlet */ @WebServlet("/Drupal2LeggedOauthServlet") public class Drupal2LeggedOauthServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String HELP_ROOT = "http://YOURDRUPALSITE/SERVICEENDPOINTPATH/node/1"; private Map<String, String> tokens = new HashMap<String, String>(); //Content types for Return private static final String HTML = "html"; private static final String XML = "xml"; private static final String JSON = "json"; private OAuthService service; @SuppressWarnings("unchecked") @Override public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { //OAuth Service, using Scribe service = new ServiceBuilder() .provider(DrupalApi.class) .apiKey("CONSUMER KEY FROM CONSUMER PAGE IN DRUPAL") .apiSecret("CONSUMER SECRET FROM CONSUMER PAGE IN DRUPAL") .callback("THE URL OF THIS PAGE") .debug() .build(); //Fetch stored tokens, if any, from session HttpSession session = req.getSession(); if (session.getAttribute("tokens") != null) { tokens = (Map<String, String>) session.getAttribute("tokens"); } //Was this called with a page request? String requestURL = req.getParameter("help_page"); if (requestURL == null) { requestURL = HELP_ROOT; } //Let's try things... try { //Get stored tokens if any String oAuthToken = tokens.get("oauth_token"); String oAuthSecret = tokens.get(oAuthToken); System.out.println("AUTH TOKEN=" + oAuthToken + " SECRET=" + oAuthSecret); // ============== GET ACCESS TOKEN ================= Token accessToken = getAccessToken(oAuthToken, oAuthSecret, session); // ============== WE'RE AUTHORIZED. GET CONTENT =============== Response response = getContent(requestURL, accessToken); // ============== WE HAVE RESPONSE, RETURN TO BROWSER ============= createHTMLResponse(resp,response, XML); } catch (Exception e) { if (e instanceof RedirectException) { RedirectException redirect = (RedirectException) e; String targetURL = redirect.url(); if (targetURL != null) { resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); resp.setHeader("Location", targetURL); } } else if (e instanceof IOException) { throw (IOException) e; } else if (e instanceof IllegalArgumentException) { throw (IllegalArgumentException) e; } } } //============== END MAIN SERVICE CALL ================== /* * Helper function to quickly get DOM document from XML string */ public static Document loadXMLFromString(String xml) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); InputSource is = new InputSource(new StringReader(xml)); return builder.parse(is); } /* * Get the Access Token for signing requests. If we don't yet have it, go * and get it from the OAuth Server, else from the session */ private Token getAccessToken(String oAuthToken, String oAuthSecret, HttpSession session) { Token accessToken; // ========= NOT AUTHORIZED YET, DO SO ============= if ((oAuthToken == null || oAuthToken.isEmpty()) || (oAuthSecret == null || oAuthSecret.isEmpty())) { System.out.println("Not Authorized, go and get those tokens"); /* * Consumer accesses protected resources by submitting OAuth signed * requests for resources using its consumer key, an empty access * token, and signs the request with the consumer secret and an * empty access token secret. */ // Get request Token (Pre authorized) Token requestToken = service.getRequestToken(); // Use empty verifier Verifier verifier = new Verifier(""); // Get access token using our pre authorized request token accessToken = service.getAccessToken(requestToken, verifier); // We have tokens now, so store them. tokens.put("oauth_token", accessToken.getToken()); tokens.put(accessToken.getToken(), accessToken.getSecret()); // Store the tokens back in the session session.setAttribute("tokens", tokens); } else { // ============ HAVE ACCESS TOKEN ALREADY ========== System.out.println("Am Authorized: Using existing token"); accessToken = new Token(oAuthToken, oAuthSecret); } return accessToken; } /* * Provided with a url and an access token, this will go and * get the content you're after. */ private Response getContent(String url, Token accessToken) { OAuthRequest request = new OAuthRequest(Verb.GET, url); service.signRequest(accessToken, request); Response response = request.send(); return response; } /* * Create the response to the browser. * Expects XML as the response format received to it. * Can return to the client in XML, HTML or JSON format * */ private void createHTMLResponse(HttpServletResponse httpResp, Response oAuthResp, String responseFormat) throws IOException,Exception{ if(responseFormat.equals(XML)){ httpResp.setContentType("text/xml"); httpResp.getWriter().println(oAuthResp.getBody()); } else if(responseFormat.equals(HTML)){ httpResp.setContentType("text/html"); httpResp.getWriter().println("<html><body>"); //Get portions of the response using XPath, the easiest way to get what we want Document doc = loadXMLFromString(oAuthResp.getBody()); XPathFactory xpathFact = XPathFactory.newInstance(); XPath xpath = xpathFact.newXPath(); Boolean arrayResult = (Boolean)xpath.evaluate("/result/@is_array",doc,XPathConstants.BOOLEAN); System.out.println("RESULT IS ARRAY? "+arrayResult); if(arrayResult){ } else{ String htmlOut = (String)xpath.evaluate("/result/body/und/item/value/text()",doc,XPathConstants.STRING); httpResp.getWriter().println(htmlOut); } httpResp.getWriter().println("</html></body>"); } else{//Else JSON httpResp.setContentType("text/json"); //Convert XML to JSON... righto... will get right on that. httpResp.getWriter().println(oAuthResp.getBody()); } } }
I’m pretty sure you don’t need the RedirectException handling code there anymore, that’s only needed if you are doing 3-Legged and need to be redirected to the authorization page.
If you do need it, I pilfered it from Google:
/** * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.doubleiq.drupaloauth; public class RedirectException extends RuntimeException { private static final long serialVersionUID = 1L; private final String url; public RedirectException(String url) { super(); this.url = url; } public String url() { return url; } }
Now, to get this visible in your servlet, go to the WEB-INF folder within WebContentand edit the web.xml file and add the following:
<servlet> <servlet-name>Drupal2LeggedOauthServlet</servlet-name> <servlet-class>yourpackagepath.drupaloauth.Drupal2LeggedOauthServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Drupal2LeggedOauthServlet</servlet-name> <url-pattern>/URLPATHYOUWANT</url-pattern> </servlet-mapping>
Now, when you go to your server and the path you specify there, you should hit your servlet (http://localhost:8080/URLPATHYOUWANT), which will then get it’s required OAuth tokens and request the resource your define in the HELP_ROOT var. You can also pass in a page you want to request using a url param of help_page=”anotherpagetoget”.
Hopefully this is enough to de-mystify the process of getting a 2-Legged OAuth system up and running using Drupal. Please do let me know anything I’ve got completely wrong, or bits I’ve missed out, or things I’ve misunderstood in terms of how things should work. I’d love for this to be an easy one-stop-shop for people trying to set up a system like mine.