nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.8k stars 3.49k forks source link

Untappd Provider #7067

Closed Kostecki closed 1 year ago

Kostecki commented 1 year ago

Description 📓

I have been trying to add Untappd (https://untappd.com/api/docs#authentication) as a provider, but what i figured would be a easy task has gotten me a bit stumped.

Untappd does authentication in the following way:

  1. Request to https://untappd.com/oauth/authenticate with _clientid and _redirecturi: https://untappd.com/oauth/authenticate/?client_id=CLIENTID&response_type=code&redirect_url=REDIRECT_URL
  2. This redirects back to REDIRECT_URL?code=code with the code parameter.
  3. Then you're supposed to make a GET request to https://untappd.com/oauth/authorize/ with _clientid, _clientsecret, _redirecturl and the code from the authenticate-request: https://untappd.com/oauth/authorize/?client_id=CLIENTID&client_secret=CLIENTSECRET&response_type=code&redirect_url=REDIRECT_URL&code=CODE which then returns the access_token that you can then use to get user info (and consume the api in general)

Doing this whole thing manually in Postman works fine. Calling the authorize-endpoint with the code returns the access_token but i'm not quite sure on how to translate that intro a provider.

How to reproduce ☕️

Basic provider code like so:

{
  id: "untappd",
  name: "Untappd",
  type: "oauth",
  authorization: "https://untappd.com/oauth/authenticate",
  token: "https://untappd.com/oauth/authorize",
  userinfo: "https://api.untappd.com/v4/user/info?compact=true",
  clientId: process.env.UNTAPPD_ID,
  clientSecret: process.env.UNTAPPD_SECRET,
  profile(profile) {
    console.log("profile", profile);
    return profile;
  }
}

All this does is get the code-paramter and then return message: 'access_token not present in TokenSet'

Contributing 🙌🏽

Yes, I am willing to help implement this feature in a PR

I wasn't quite sure where to put this as it's sort of a feature request, but also me trying to actually complete the request 🤷‍♂️

balazsorban44 commented 1 year ago

Trying to have a look but after registering, I get the following under register:

Thank you for your interest in Untappd’s API. At this time, we are no longer accepting new applications for API access as we work to improve our review and support processes. We do not have a planned date to begin accepting new applications, so please check back soon.

If you have credentials (client id and client secret) to loan, reach out to me on Twitter (or e-mail, etc.) and I can have a closer look.

My suspicion is that although they claim to be OAuth 2 compliant, the https://untappd.com/oauth/authorize endpoint (which should semantically rather be called /token) does not return a spec-compliant token response. https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4

It puts the access_token under an object response for some reason, likely causing the 'access_token not present in TokenSet' error to be thrown.

I'll go ahead and guess that you can fix it by the following:

@auth/core:

{
  id: "untappd",
  name: "Untappd",
  type: "oauth",
  authorization: "https://untappd.com/oauth/authenticate",
  token: {
    url: "https://untappd.com/oauth/authorize",
    async conform(response) {
      if (response.ok) {
        const body = await response.clone().json()
        if (body?.response?.access_token) {
          return new Response(JSON.stringify(body.response), response)
        } else if (body?.access_token) {
          console.warn("Token response conforms to the standard, workaround not needed.")
        }
      }
      return response
    },
  },
  userinfo: "https://api.untappd.com/v4/user/info?compact=true",
}

next-auth:

{
  id: "untappd",
  name: "Untappd",
  type: "oauth",
  authorization: "https://untappd.com/oauth/authenticate",
  token: {
    url: "https://untappd.com/oauth/authorize",
    async request({ client, params, checks, provider }) {
      const tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
      if(tokens.response) {
        return { tokens: tokens.response }
      }
      console.warn("Token response conforms to the standard, workaround not needed.")
      return { tokens }
    },
  },
  userinfo: "https://api.untappd.com/v4/user/info?compact=true",
}

Assuming you set the redirect URL to http://localhost:3000/api/auth/callback/untappd, this should hopefully work. Please let me know.

balazsorban44 commented 1 year ago

Thanks for contacting me on Twitter. I was able to have a look, here is the final, working config! 🍻:

{
  id: "untappd",
  name: "Untappd",
  type: "oauth",
  clientId: process.env.UNTAPPD_ID,
  clientSecret: process.env.UNTAPPD_SECRET,
  authorization: "https://untappd.com/oauth/authenticate",
  token: {
    async request({ params, provider }) {
      const tokenEndpoint = `https://untappd.com/oauth/authorize?${new URLSearchParams({
        ...params,
        client_id: provider.clientId,
        client_secret: provider.clientSecret,
        redirect_url: provider.callbackUrl,
      })}`
      const response = await fetch(tokenEndpoint)
      const tokens = await response.json()
      return { tokens: tokens.response }
    },
  },
  userinfo: "https://api.untappd.com/v4/user/info",
  profile(profile) {
    const { user } = profile.response
    return {
      id: user.id,
      name: user.user_name,
      email: user.settings.email_address,
      image: user.user_avatar_hd,
    }
  },
},

Proof: image

Unfortunately, the token endpoint deviates so much from the spec, that it might be complicated to implement this for @auth/core, subsequently @auth/nextjs when that takes next-auth over, since we are trying to enforce spec-compliance. In this case, the conform method only is able to correct the code exchange response, but Untappd even assumes the wrong request format.

It requires a GET request to the authorization endpoint, with client_id, client_secret, and redirect_uri search params, while the spec would expect a POST request as described here: https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3

Assuming next-auth is good enough for your current use case, I will close this issue for now. Cheers 🍻!

PS.: I reached out to Untappd on Twitter and in an email to hear back if they would be open to adhering to the spec. Will let you know if they answer.

Kostecki commented 1 year ago

Absolutely amazing. I ended up having to do none of the work and still got all of the rewards! I fully understand your stance on atually adding them as a provider - hopefully they'll get their act together some day 🍻