Ben Biddington

Whatever it is, it's not about "coding"

Facebook Graph API — getting access tokens

with 73 comments

As described in the documentation it’s a fairly easy process — and it does not require any signatures.

For example, I have an application with an id 116122545078207, and I am using the URL of this blog (https://benbiddington.wordpress.com) to collect request tokens.

[update, 2010-04-29]

If ever there was a lesson to read specification and documentation carefully, this is it. Thanks to comments from Joshua Inkenbrandt, Alex and Gene Leybzon I realise why my examples don’t work as expected: I have been trying to use a mixture of web server and client_cred authentication flow.

As Gene rightly points out, I should have been using user_agent.

User-Agent Flow (with a web browser)

Following the instructions as specified in section 3.5.1.1., Client Requests Authorization, of the specification, this is a one-step process:

Open this in a browser:

https://graph.facebook.com/oauth/authorize?
    type=user_agent&
    client_id=116122545078207&
    redirect_uri=http%3A%2F%2Fbenbiddington.wordpress.com&
    scope=user_photos,email,user_birthday,user_online_presence

Note: there are several options for scope. These are called extended permissions.

Note: unless you specify offline_access, your tokens will expire as soon as the user signs out of facebook.

Note: client_secret is not supplied:

[3.5.1.  User-Agent Flow] This user-agent flow does not utilize the client secret since the client executables reside on the end user’s computer or device which makes the client secret accessible and exploitable.

You’ll be redirected to:

https://benbiddington.wordpress.com/#access_token=
    116122545078207|
    2.1vGZASUSFMHeMVgQ_9P60Q__.3600.1272535200-500880518|
    QXlU1XfJR1mMagHLPtaMjJzFZp4.

And you have your access token, you can go ahead and use it:

https://graph.facebook.com/me?access_token=
    116122545078207|
    2.1vGZASUSFMHeMVgQ_9P60Q__.3600.1272535200-500880518|
    QXlU1XfJR1mMagHLPtaMjJzFZp4.

According to section 3.5.1. Client Requests Authorization, because we have not supplied the optional secret_type:

secret_type
    OPTIONAL. The access token secret type as described by Section 5.3.
    If omitted, the authorization server will issue a bearer token
    (an access token without a matching secret) as described by Section 5.2.

we have been issued a bearer token.

I think this refers to OAuth 1.0-style authentication using token secret. You’d only need one of those if you were requiring signed requests. This seems to contradict the part above about storing client secret on user agent.

Refreshing tokens

Section 3.5.1 describes that the access token may be delivered with an optional refresh_token fragment. On expiry, this token can be exchanged at the for a new access token. No refresh token is supplied by the Facebook API under User-Agent flow, meaning you’ll have to ask users to sign in again.

Using access tokens

Tokens with no session part

In some cases, like when using 3.7.1.  Client Credentials Flow, you’re issued a token with a missing session part.

Instead of this:

    116122545078207|
    2.1vGZASUSFMHeMVgQ_9P60Q__.3600.1272535200-500880518|
    EyWJJYqrdgQgV1bfueck320z7MM.

you get this:

116122545078207|EyWJJYqrdgQgV1bfueck320z7MM.

These do work in some cases, but are rejected by some resources, for example:

https://graph.facebook.com/me?access_token=116122545078207|EyWJJYqrdgQgV1bfueck320z7MM.

returns  error:

{
   "error": {
      "type": "QueryParseException",
      "message": "An active access token must be used to query information about the current user."
   }
}

this is the same error you get when you request the same resource without supplying a token at all:

http://graph.facebook.com/me

Note that these tokens do work against real resource identifer, i.e., without the me alias. For example, here I can use it against me (benbiddington).

https://graph.facebook.com/benbiddington?access_token=116122545078207|EyWJJYqrdgQgV1bfueck320z7MM.

So the me alias only works in the case where we have a full token — the session part is required.

These tokens also work for accessing your insights (see analytics section):

https://graph.facebook.com/app_id/insights?access_token=116122545078207|EyWJJYqrdgQgV1bfueck320z7MM.

This is described in section 3.7.  Autonomous Client Flows:

Autonomous client flows are used to grant client access to protected resources controlled by the client (i.e. the client is the resource owner). For example, these flows are useful when a service provides both client-specific resources in addition to end user resources.

And more specifially, the Client Credentials Flow is described  in section 3.7.1:

The client credentials flow is used when the client acts autonomously without acting on behalf of a separate resource owner. The client secret is assumed to be high-entropy since it is not designed to be memorize by an end user.

Where a client is:

An HTTP client capable of making authenticated requests for protected resources using the OAuth protocol. [This is third-party application that wants to access a resource owner’s Facebook account.]

And a resource owner:

An entity capable of granting access to a protected resource. [This is the user who owns the Facebook account.]

[TBD: So what?]

Tokens, sessions and that

You can see more information about authentication flow by using a bogus redirect_uri, i.e., one that does not match the Connect URL setting in your application, e.g.:

https://graph.facebook.com/oauth/authorize?
    client_id=116122545078207&
    redirect_uri=http%3A%2F%2Flocalhost&
    scope=user_photos

Executing this gives error:

{
   "error": {
      "type": "OAuthException",
      "message": "Invalid redirect_uri: The Facebook Connect cross-domain
          receiver URL (http://localhost) must have the application's
          Connect URL (https://benbiddington.wordpress.com) as a prefix.
          You can configure the Connect URL in the Application Settings Editor."
   }
}

But there is some information in the query string, that when decoded looks like this:

https://graph.facebook.com/oauth/authorize_success?
    client_id=116122545078207&
    redirect_uri=http://localhost&
    scope=user_photos&
    type=web_server&
    perms=user_photos&
    selected_profiles=500880518&
    session={
        "session_key":"2.vHAZRg0Ac4Dtzm2xiVwXoA__.3600.1272286800-500880518",
        "uid":500880518,
        "expires":1272286800,
        "secret":"vHAZRg0Ac4Dtzm2xiVwXoA__",
        "sig":"7a6fc887240884de883a21e2a2aec3e0"
    }

That session_key:

2.vHAZRg0Ac4Dtzm2xiVwXoA__.3600.1272286800-500880518

looks familiar, it’s the same as the code parameter used in web server authentication flow (Section 3.5.2), and it’s the same pattern as the second segment of an access token.

2.{secret}.3600.{expires_at_seconds_after_epoch}-{user_id}

Where are my extended permissions?

It appears there is some problem with authorizing extended permissions.

For example, when I use the link in step (1) up there, I am prompted with the following screen:

That looks like the set I asked for, and so I select Allow.

But when I then inspect the extended permissions in my Application settings, all I see is this:

For some reason the only extra permission I have is email. And actually, did I even ask for Publish recent activity?

What is wrong here?

Troubleshooting

I can’t see my application in my Application settings screen

Make sure to choose Authorised from the show list on the Application settings screen.

The default view is Recently used which — certainly in my case — does not produce my application.

How do I know what permissions my application has?

Once you find the application in your Application settings list, press Edit settings and then select the Additional Permissions tab on the resultant dialog.

How do I de-authorise an application?

From your Application settings list, press the “x” button the right end of the row next to Application Profile link.

My access tokens only seem valid while a user is signed-in to facebook

You need to request offline_access permissions, e,g,:

https://graph.facebook.com/oauth/authorize?
    type=user_agent&
    client_id=116122545078207&
    redirect_uri=http%3A%2F%2Fbenbiddington.wordpress.com&
    scope=user_photos,email,user_birthday,user_online_presence,offline_access

otherwise your access tokens will expire as soon as the user signs out and you’ll get (at least with client):

{
    "error": {
        "type": "OAuthException",
        "message": "Error processing access token."
    }
}

References

Advertisements

Written by benbiddington

23 April, 2010 at 13:37

Posted in development

Tagged with , , , ,

Passing cucumber specs on windows

leave a comment »

I had some trouble getting cucumber test features to pass on Windows due to directory separator differences, something like:
 Failing Scenarios:
-cucumber features/background/failing_background.feature:8 # Scenario: failing background
+cucumber features\background\failing_background.feature:8 # Scenario: failing background
There is an easy solution — run features with windows_mri profile, which results in the CUCUMBER_FORWARD_SLASH_PATHS environment variable being set to true.

Example

cucumber features/background.feature:93 -p windows_mri

Written by benbiddington

20 April, 2010 at 13:37

Posted in development

Tagged with , , ,

Raking .NET projects in TeamCity

leave a comment »

Faced with the unpleasant prospect of assembling yet another stack of xml files for an automated build, I thought I’d try rake instead. A couple of people here at 7digital have used Albacore before, so I started there.

1. Build

Use Albacore‘s msbuild task:

require 'albacore'

desc "Clean and build"
msbuild 'clean_and_build' do |msb|
    msb.properties :configuration => :Release
    msb.targets :Clean, :Build
    msb.verbosity = "quiet"
    msb.solution  = "path/to/ProjectName.sln"
end

2. Run tests

This is also very straight forward with Albacore, but slightly more useful is applying the usual TeamCity test result formatting and reporting.

2.1 Tell your build where the NUnit test launcher is

TeamCity already has an NUnit runner, and the recommended way to reference it is with an environment variable.

Note: The runners are in the <TEAM CITY INSTALLATION DIR>/buildAgent/plugins/dotnetPlugin/bin directory.

2.2 Write the task

Once you have the path to the executable, you’re free to apply any of the available runner options.

Assuming you have added the TEAMCITY_NUNIT_LAUNCHER environment variable then the actual execution is then something like:

asm = 'ProjectName.Unit.Tests.dll'
nunit_launcher = ENV["TEAMCITY_NUNIT_LAUNCHER"]
sh("#{nunit_launcher} v2.0 x86 NUnit-2.5.0 #{asm}")

Beats hundreds of lines of xml I reckon.

References

Written by benbiddington

18 February, 2010 at 13:37

Posted in development

Tagged with , , , , , ,

Serialization rules for Adobe Content Server

with 31 comments

Working with Adobe Content Server can be a truly depressing experience. The recommendation is to use a jar file — UploadTestJar — written by Adobe to perform HTTP RPC operations against the Content Server.

Problem is that UploadTestJar only does uploads, but we need full control, like deletes for example. Porting the java is possible, but it’s some of the most poorly written crap I have ever seen, and finding a specification is resisting web search.

Finally we managed to get a description from the support staff which’ll be helpful if you’re intending to port that awful UploadTestJar mess.

  1. All adjacent text nodes are collapsed and their leading and trailing whitespace is removed.
  2. Zero-length text nodes are removed.
  3. Signature elements in Adept namespace are removed.
  4. Attributes are sorted first by their namespaces and then by their names; sorting is done byte wise on UTF-8 representations.
    1. If attributes have no namespace insert a 0 length string (i.e. 2 bytes of 0) for the namespace
  5. Strings are serialized by writing two-byte length (in big endian order) of the UTF-8 representation and then UTF-8 representation itself
  6. Long strings (longer than 0x7FFF) are broken into chunks: first as many strings of the maximum length 0x7FFF as needed, then the remaining string. This is done on the byte level, irrespective of the UTF-8 boundary.
  7. Text nodes (text and CDATA) are serialized by writing TEXT_NODE byte and then text node value.
  8. Attributes are serialized by writing ATTRIBUTE byte, then attribute namespace (empty string if no namespace), attribute name, and attribute value.
  9. Elements are serialized by writing BEGIN_ELEMENT byte, then element namespace, element name, all attributes END_ATTRIBUTES byte, all children, END_ELEMENT byte.

This list is in actually the javadocs for the XmlUtil class. Why it’s all lumped in there is anybody’s guess. The serialization as described above is mostly implemented by one very long method in (1000+ line) XmlUtil.java: Eater.eatNode.

Note: The values of the constants BEGIN_ELEMENT etc are listed in the XMLUtil class.

Why I consider UploadTestJar poorly written

Here are some things I’ve noticed:

  • Nothing reads like a narrative, i.e. , methods call other methods that occur before it in the file — makes files very hard to follow.
  • Too many comments. I know this is a java idiom, but it make reading the stuff that matter more difficult
  • Idiotic comments: inline comments that state the obvious and are just noise. e.g.:// retrieve HMAC key and run a raw SHA1 HASH on it.
    byte[] hmacKeyBytesSHA1 = XMLUtil.SHA1(getHmacKey());
  • XMLUtil.java contains several classes
  • XMLUtil class does more than one thing:
    • Parses XML
    • Normalizes XML
    • Creates XML documents
    • Serializes XML, dates, bytes and strings
    • Checks signatures
    • Signs XML documents
    • Hashes things
  • Class UploadTest does everything in ctor: reads a file from disk, validates it, makes some xml, signs it and then posts it to the server.
  • UploadTest the main entry point for executable, and it contains all the behaviour — it’s 1600 lines long
  • Cannot use UploadTest without a real epub file
  • UploadTest does too many things:
    • Ctor does too many things
      • Handles command line input
      • Displays help/usage
      • Asserts a file on disk has been supplied
      • “Makes” content
        • makeContent requires a file an epub on disk
        • makeContent loads xml
        • makeContent assembles xml files
        • makeContent hashes things
        • makeContent swallows errors and writes to stdout
    • “Sends” content via HTTP
    • Methods that do too many things, e.g., if/else branches based on the verboseDisplay flag

Written by benbiddington

16 February, 2010 at 10:39

I’ve created my gem, but can’t require it

with one comment

Creating gems is easy, all you need is a gemspec:

Gem::Specification.new do |spec|
    spec.name = 'chubby-bat'
    spec.version = '0.0.1337'

    spec.files = [
	'./lib/chubby_rain.rb',
	'./lib/crack_murphy.rb',
    ]

    spec.summary = 'An example gem'
    spec.author = 'Ben Biddington'
end

And then to build it:

$ gem build chubby-bat.gemspec

And then to install it:

$ gem install chubby-bat-0.0.1337.gem

No such file to load — xxx

Trouble is, when requiring the new gem, you may encounter an error like:

irb(main):001:0> require 'chubby-bat'
LoadError: no such file to load -- chubby-bat
        from C:/Ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in
            `gem_original_require'
        from C:/Ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
        from (irb):1
irb(main):002:0>

I found this pretty confusing, until I realised the interpreter is looking for a file called chubby-bat.

In short, a gem needs to contain a file with the same name as the gem itself in its lib folder:

And gemspec becomes:

Gem::Specification.new do |spec|
    spec.name = 'chubby-bat'
    spec.version = '0.0.1337'

    spec.files = [
	'./lib/chubby-bat.rb',
	'./lib/chubby_rain.rb',
	'./lib/crack_murphy.rb',
    ]

    spec.summary = 'An example gem'
    spec.author = 'Ben Biddington'
end

Rubygems require

The comment in rubygems custom_require explains:

# See: path/to/Ruby/lib/ruby/site_ruby/1.8/rubygems, custom_require

##
  # When RubyGems is required, Kernel#require is replaced with our own which
  # is capable of loading gems on demand.
  #
  # When you call require 'x', this is what happens:
  # * If the file can be loaded from the existing Ruby loadpath, it
  #   is.
  # * Otherwise, installed gems are searched for a file that matches.
  #   If it's found in gem 'y', that gem is activated (added to the
  #   loadpath).
  #
  # The normal require functionality of returning false if
  # that file has already been loaded is preserved.

Ruby loadpath, gem install and Kernel#require

Installing a gem simply copies files to somewhere in $LOAD_PATH, and invoking require triggers a file search within these locations.

Kernel require does the following:

  • Ruby tries to load the library named string, returning true if successful.
  • If the filename does not resolve to an absolute path, it will be searched for in the directories listed in $:.
  • If the file has the extension “.rb’’, it is loaded as a source file; if the extension is “.so’’, “.o’’, or “.dll’’,or whatever the default shared library extension is on the current platform, Ruby loads the shared library as a Ruby extension. Otherwise, Ruby tries adding “.rb’’, “.so’’, and so on to the name.
  • The name of the loaded feature is added to the array in $”.
  • A feature will not be loaded if it‘s name already appears in $”.

This means if you print out the contents of $”, you’ll see chubby-bat.rb in there.

Written by benbiddington

12 January, 2010 at 13:37

Posted in development

Tagged with , , , ,

Git — remove all deleted files in one line

with one comment

I got sick of manually removing deleted files, this makes it easier:

$ git rm $(git status | grep deleted: | awk '{ print $3 }')

Written by benbiddington

3 January, 2010 at 13:15

Posted in development

Tagged with , ,

Network diagnostics

leave a comment »

If you have a problem like:

  • One of a set of load balanced servers is behaving unexpectedly and
  • Targetting that machine directly by IP address does not fail the same way

Then you may have a missing host header.

Your host name may be being routed to a different application.

This kind of thing has prompted us to think about writing smoke tests for this type of multi-machine configuration.

Written by benbiddington

2 November, 2009 at 19:04