javascalaoauth-2.0pocket

How to do OAuth authentication from Java/Scala when we know username/password?


tl;dr - I want to authenticate against an OAuth 2.0 API with my own username/password from a desktop app (do not want to open browser) from Java/Scala.

Why? I want to authenticate with Pocket's v3 API from Java/Scala using my own credentials and fetch my unread items. This is for a personal command line tool that I do not intend to release for general use. They used to have a nice basic-auth API but they deprecated it and introduced OAuth 2.0 and I am not sure how to do what I want anymore.


Solution

  • Okay, so it turns out that this is possible provided a few things are in place first. You will need to register an app with pocket first in order to get a consumer key. You can do that here:

    http://getpocket.com/developer/apps/new

    Then, manually go through the steps 2 and 3 at the link below to get your new app approved for your pocket account one time. This is a one time manual step after which things can be automated. I used curl for step 2 and my browser (chrome) for step 3:

    http://getpocket.com/developer/docs/authentication

    Then, you will need to find your pocket user id value. For me, it was in a cookie tied to the domain "getpocket.com" and had the name sess_user_id. Armed with your consumer key and user if, you can then use the following code to obtain an auth token for making calls to pocket. Note that I am using dispatch 0.10.0 and spray-json 1.2.3 as 3rd party libs:

    import dispatch._
    import spray.json._
    import scala.concurrent.ExecutionContext
    import java.util.concurrent.Executors
    import scala.concurrent.Await
    import scala.concurrent.duration._
    import com.ning.http.client.Cookie
    
    case class CodeRequest(consumer_key:String, redirect_uri:String = "fake:uri")
    object CodeRequest
    case class CodeResponse(code:String)
    object CodeResponse
    
    case class AuthRequest(consumer_key:String, code:String)
    object AuthRequest
    case class AuthResponse(access_token:String, username:String)
    object AuthResponse
    
    object PocketJsonProtocol extends DefaultJsonProtocol {
      implicit val codeRequestFormat = jsonFormat2(CodeRequest.apply)
      implicit val codeResponseFormat = jsonFormat1(CodeResponse.apply)
      implicit val authRequestFormat = jsonFormat2(AuthRequest.apply)
      implicit val authResponseFormat = jsonFormat2(AuthResponse.apply)  
    }
    
    object PocketAuth {
      import PocketJsonProtocol._
      val JsonHeaders = Map("X-Accept" -> "application/json", "Content-Type" -> "application/json; charset=UTF-8")
      implicit val EC = ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
    
      def authenticate(consumerKey:String, userId:String) = {
        val fut = for{
          codeResp <- requestCode(consumerKey)
          _ <- activateToken(codeResp, userId)
          authResp <- requestAuth(consumerKey, codeResp)
        } yield{
          JsonParser(authResp).convertTo[AuthResponse]
        }
    
        val auth = Await.result(fut, 5 seconds)
        auth.access_token
      }
    
      def requestCode(key:String) = {
        val req = url("https://getpocket.com/v3/oauth/request") <:< JsonHeaders << CodeRequest(key).toJson.toString
        Http(req.POST OK as.String).map(JsonParser(_).convertTo[CodeResponse])
      }
    
      def activateToken(codeResp:CodeResponse, userId:String) = {
        val req = (url("https://getpocket.com/auth/authorize") <<? Map("request_token" -> codeResp.code, "redirect_uri" -> "foo")).addCookie(
          new Cookie(".getpocket.com", "sess_user_id", userId, "/", 100, false))
        Http(req)
      }
    
      def requestAuth(key:String, codeResp:CodeResponse) = {
        val req = url("https://getpocket.com/v3/oauth/authorize") <:< JsonHeaders << AuthRequest(key, codeResp.code).toJson.toString
        Http(req.POST OK as.String)
      }
    
    }
    

    All you need to do is call the authenticate function on the PocketAuth object and that will return the String auth token.

    Is it a little kludgy? Yes, but that's because they don't really want you automating this process, but it is possible as I got it to work repeatedly.