freckle/yesod-auth-oauth2

Adding state parameter causes malformed URL

Closed this issue · 5 comments

I'm building a simple intgration for LinkedIn. Everything is working except that there it generates a slightly malformed URL. The authorize endpoint ends up looking like this:

https://www.linkedin.com/uas/oauth2/authorization&state=rfvjuhwswetbcdwzfeoghnxhovhjjz?client_id=7730si35gvu8rr&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fpage%2Flinkedin%2Fcallback

The ampersand after the word authorization needs to be a question mark, and the question mark after the state parameter needs to be an ampersand.

I've tracked the source of the error down to this piece of the code: https://github.com/thoughtbot/yesod-auth-oauth2/blob/4afaba6645a0df69c4cf0760ff7ad56c69057bc2/Yesod/Auth/OAuth2.hs#L89-L90

My code that uses the library looks like this:

oauth2LinkedIn clientId clientSecret = authOAuth2 "linkedin"
    OAuth2
        { oauthClientId = encodeUtf8 clientId
        , oauthClientSecret = encodeUtf8 clientSecret
        , oauthOAuthorizeEndpoint = "https://www.linkedin.com/uas/oauth2/authorization"
        , oauthAccessTokenEndpoint = "https://www.linkedin.com/uas/oauth2/accessToken"
        , oauthCallback = Nothing
        }
    $ fromProfileURL
        "linkedin"
        "https://api.linkedin.com/v1/people/~?format=json"
        credsFromLinkedInUser

Thanks for reporting this; I think a number of plugins may have been broken in this way for quite some time (since the state parameter was introduced). I've got what I hope is a fix in the referenced PR.

I tested the commit the referenced, and it does fix the issue.

jezen commented

@andrewthad How did you make this work? I have code like this:

data LinkedInUser = LinkedInUser
  { linkedinUserGivenName :: Text
  , linkedinUserFamilyName :: Text
  , linkedinUserEmail :: Text
  }

instance FromJSON LinkedInUser where
  parseJSON (Object v) = LinkedInUser
    <$> v .: "firstName"
    <*> v .: "lastName"
    <*> v .: "emailAddress"
  parseJSON _ = empty

oauth2LinkedIn :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2LinkedIn clientId clientSecret= authOAuth2 "linkedin" oauth makeCredentials
  where
    oauth = OAuth2
      { oauthClientId = encodeUtf8 clientId
      , oauthClientSecret = encodeUtf8 clientSecret
      , oauthOAuthorizeEndpoint = "https://www.linkedin.com/oauth/v2/authorization"
      , oauthAccessTokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken"
      , oauthCallback = Nothing
      }
    makeCredentials = fromProfileURL
      "linkedin"
      "https://api.linkedin.com/v1/people/~?format=json"
      credsFromLinkedInUser

credsFromLinkedInUser :: LinkedInUser -> Creds m
credsFromLinkedInUser user = Creds
  { credsPlugin = "linkedin"
  , credsIdent = linkedinUserEmail user
  }

…But every time I try to authenticate, it fails on the callback with:

StatusCodeException (Status {statusCode = 401, statusMessage = "Unauthorized"}) [("Server","Apache-Coyote/1.1"),("x-li-request-id","BSFLTA89H2"),("Date","Tue, 06 Dec 2016 10:45:08 GMT"),("Vary","*"),("x-li-format","json"),("Content-Type","application/json;charset=UTF-8"),("Content-Encoding","gzip"),("X-Li-Fabric","prod-lor1"),("Transfer-Encoding","chunked"),("Connection","keep-alive"),("X-Li-Pop","PROD-IDB2"),("Set-Cookie","lidc=\"b=OB95:g=26:u=41:i=1481021109:t=1481100618:s=AQEUH5OqBzP-4npQrvrCuWNG2j364fgI\"; Expires=Wed, 07 Dec 2016 08:50:18 GMT; domain=.linkedin.com; Path=/"),("X-LI-UUID","0eb8EdqkjRSAoJJFDisAAA=="),("X-Response-Body-Start","{\n \"errorCode\": 0,\n \"message\": \"Unable to verify access token\",\n \"requestId\": \"BSFLTA89H2\",\n \"status\": 401,\n \"timestamp\": 1481021109219\n}"),("X-Request-URL","GET https://api.linkedin.com:443/v1/people/~?format=json")] (CJ {expose = [Cookie {cookie_name = "lidc", cookie_value = "\"b=OB95:g=26:u=41:i=1481021109:t=1481100618:s=AQEUH5OqBzP-4npQrvrCuWNG2j364fgI\"", cookie_expiry_time = 3016-04-08 00:00:00 UTC, cookie_domain = "linkedin.com", cookie_path = "/", cookie_creation_time = 2016-12-06 10:45:09.264807 UTC, cookie_last_access_time = 2016-12-06 10:45:09.264807 UTC, cookie_persistent = False, cookie_host_only = False, cookie_secure_only = False, cookie_http_only = False}]})

I found that apparently LinkedIn doesn't do OAuth2 properly: http://stackoverflow.com/questions/28094926/linkedin-oauth2-unable-to-verify-access-token

And here I found some code that may encourage LinkedIn to behave: https://github.com/ip1981/sproxy2/blob/master/src/Sproxy/Application/OAuth2/LinkedIn.hs

Here is my code from, what is at this point in time, a fairly old project that I am barely maintaining. I have no recollection of even writing it. Also, this feature was only really used once about a year ago, and LinkedIn's API may have changed since I wrote this, so it might not actually work any more. The main difference I can see is that we are using different endpoints:

data LinkedInUser = LinkedInUser
  { linkedInUserId :: Text
  }

instance FromJSON LinkedInUser where
  parseJSON (Object o) = LinkedInUser
    <$> o .: "id"
  parseJSON _ = mzero

-- Should probably improve the information that is retrieved
oauth2LinkedIn ::
  ( YesodAuth m
  ) => Text -- ^ Client ID
    -> Text -- ^ Client Secret
    -> AuthPlugin m
oauth2LinkedIn clientId clientSecret = authOAuth2 "linkedin"
    OAuth2
        { oauthClientId = encodeUtf8 clientId
        , oauthClientSecret = encodeUtf8 clientSecret
        , oauthOAuthorizeEndpoint = "https://www.linkedin.com/uas/oauth2/authorization"
        , oauthAccessTokenEndpoint = "https://www.linkedin.com/uas/oauth2/accessToken"
        , oauthCallback = Nothing
        }
    $ fromProfileURL
        "linkedin"
        "https://api.linkedin.com/v1/people/~?format=json"
        credsFromLinkedInUser

credsFromLinkedInUser :: LinkedInUser -> Creds m
credsFromLinkedInUser u = Creds
  { credsPlugin = "linkedin"
  , credsIdent = linkedInUserId u
  , credsExtra = []
  }
jezen commented

@andrewthad Wow, thanks for getting back to me so quickly.

Yes, I spotted the endpoint difference, and I realised that it works with the ones you use and not with the ones in LinkedIn's documentation.

I'll see if I can pull out some more user information, and then I'll most likely submit a PR to this repo for a LinkedIn plugin.