nextauthjs / next-auth

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

Custom Provider broken by V5 #10852

Open Nik-Novak opened 2 months ago

Nik-Novak commented 2 months ago

Environment

System:
    OS: Linux 5.15 Ubuntu 20.04.6 LTS (Focal Fossa)
    CPU: (16) x64 AMD Ryzen 9 5950X 16-Core Processor
    Memory: 5.12 GB / 15.75 GB
    Container: Yes
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 20.12.0 - ~/.nvm/versions/node/v20.12.0/bin/node
    Yarn: 1.22.22 - ~/.nvm/versions/node/v20.12.0/bin/yarn
    npm: 10.5.0 - ~/.nvm/versions/node/v20.12.0/bin/npm
  npmPackages:
    @auth/prisma-adapter: ^1.5.1 => 1.5.1 
    next: ^14.2.3 => 14.2.3 
    next-auth: ^5.0.0-beta.17 => 5.0.0-beta.17 
    react: ^18.3.1 => 18.3.1

Reproduction URL

https://github.com/Nik-Novak/Mind-Knight

Describe the issue

I use a custom provider (https://github.com/Nekonyx/next-auth-steam) to enable steam sign in using steam's OpenId 2.0 support (not OpenID connect)

After requiring an upgrade to V5, the provider no longer works with the following:

[auth][error] InvalidEndpoints: Provider "steam" is missing both `issuer` and `token` endpoint config. At least one of them is required.. Read more at https://errors.authjs.dev#invalidendpoints
    at assertConfig (webpack-internal:///(rsc)/./node_modules/next-auth/node_modules/@auth/core/lib/utils/assert.js:92:24)
    at Auth (webpack-internal:///(rsc)/./node_modules/next-auth/node_modules/@auth/core/index.js:88:95)

I've rewritten the provider to match any new formats but to no avail:

import { randomUUID } from "crypto"
import { NextApiRequest } from "next"
import { AUTHORIZATION_URL, EMAIL_DOMAIN, LOGO_URL, PROVIDER_ID, PROVIDER_NAME, SteamProfile } from "next-auth-steam"
import { SteamProviderOptions } from "next-auth-steam/lib/steam"
import { OAuthConfig, OAuthUserConfig } from "next-auth/providers"
import { NextRequest } from "next/server"
import { RelyingParty } from 'openid'
import { TokenSet } from 'openid-client'

export function Steam(
  req: Request | NextRequest | NextApiRequest,
  options: SteamProviderOptions
): OAuthConfig<SteamProfile> {
  const callbackUrl = new URL(options.callbackUrl)

  // https://example.com
  // https://example.com/api/auth/callback/steam
  const realm = callbackUrl.origin
  const returnTo = `${callbackUrl.href}/${PROVIDER_ID}`

  if (!options.clientSecret || options.clientSecret.length < 1) {
    throw new Error(
      'You have forgot to set your Steam API Key in the `clientSecret` option. Please visit https://steamcommunity.com/dev/apikey to get one.'
    )
  }

  return {
    clientSecret: options.clientSecret,

    // options: options as OAuthUserConfig<SteamProfile>,
    id: PROVIDER_ID,
    name: PROVIDER_NAME,
    type: 'oauth',
    style: {
      logo: LOGO_URL,
      // logoDark: LOGO_URL,
      bg: '#000',
      text: '#fff',
      // bgDark: '#000',
      // textDark: '#fff'
    },
    // idToken: false,
    checks: ['none'],
    clientId: PROVIDER_ID,
    authorization: {
      url: AUTHORIZATION_URL,
      params: {
        'openid.mode': 'checkid_setup',
        'openid.ns': 'http://specs.openid.net/auth/2.0',
        'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
        'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
        'openid.return_to': returnTo,
        'openid.realm': realm
      }
    },
    token: {
      async request() {
        console.log('RUN token');
        if (!req.url) {
          throw new Error('No URL found in request object')
        }

        const identifier = await verifyAssertion(req, realm, returnTo)

        if (!identifier) {
          throw new Error('Unauthenticated')
        }

        return {
          tokens: new TokenSet({
            id_token: randomUUID(),
            access_token: randomUUID(),
            steamId: identifier
          })
        }
      }
    },

    userinfo: {
      async request(ctx) {
        console.log('RUN userinfo.request');
        const url = new URL('https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002')

        url.searchParams.set('key', ctx.provider.clientSecret as string)
        url.searchParams.set('steamids', ctx.tokens.steamId as string)

        const response = await fetch(url)
        const data = await response.json()
        console.log('PROFILE',data.response.players[0] );
        return data.response.players[0]
      }
    },
    profile(profile: SteamProfile) {
      console.log('RUN PROFILE');
      // next.js can't serialize the session if email is missing or null, so I specify user ID
      return {
        id: profile.steamid,
        image: profile.avatarfull,
        email: `${profile.steamid}@${EMAIL_DOMAIN}`,
        name: profile.personaname
      }
    }
  }
}

/**
 * Verifies an assertion and returns the claimed identifier if authenticated, otherwise null.
 */
async function verifyAssertion(
  req: Request | NextRequest | NextApiRequest,
  realm: string,
  returnTo: string
): Promise<string | null> {
  // Here and from here on out, much of the validation will be related to this PR: https://github.com/liamcurry/passport-steam/pull/120.
  // And accordingly copy the logic from this library: https://github.com/liamcurry/passport-steam/blob/dcebba52d02ce2a12c7d27481490c4ee0bd1ae38/lib/passport-steam/strategy.js#L93
  const IDENTIFIER_PATTERN = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/
  const OPENID_CHECK = {
    ns: 'http://specs.openid.net/auth/2.0',
    claimed_id: 'https://steamcommunity.com/openid/id/',
    identity: 'https://steamcommunity.com/openid/id/'
  }

  // We need to create a new URL object to parse the query string
  // req.url in next@14 is an absolute url, but not in next@13, so example.com used as a base url
  const url = new URL(req.url!, 'https://example.com')
  const query = Object.fromEntries(url.searchParams.entries())

  if (query['openid.op_endpoint'] !== AUTHORIZATION_URL || query['openid.ns'] !== OPENID_CHECK.ns) {
    return null
  }

  if (!query['openid.claimed_id']?.startsWith(OPENID_CHECK.claimed_id)) {
    return null
  }

  if (!query['openid.identity']?.startsWith(OPENID_CHECK.identity)) {
    return null
  }

  const relyingParty = new RelyingParty(returnTo, realm, true, false, [])

  const assertion: {
    authenticated: boolean
    claimedIdentifier?: string | undefined
  } = await new Promise((resolve, reject) => {
    relyingParty.verifyAssertion(req, (error, result) => {
      if (error) {
        reject(error)
      }

      resolve(result!)
    })
  })

  if (!assertion.authenticated || !assertion.claimedIdentifier) {
    return null
  }

  const match = assertion.claimedIdentifier.match(IDENTIFIER_PATTERN)

  if (!match) {
    return null
  }

  return match[1]
}

How to reproduce

clone that repo, provide a .env file with the following:

NODE_ENV="development" MINDNIGHT_WS="ws://thehardcoders.de:6543" DATABASE_URL="ANY MONGO DATABASE URL" NEXTAUTH_SECRET="5Esdlji1tsasdasgLGZMxqOkF32345rdfs1dxcKzkVtfY=" NEXTAUTH_DISCORD_CLIENTID="234234234234" NEXTAUTH_DISCORD_SECRET="UcADFasfasF234GYCW4" NEXTAUTH_STEAM_SECRET="1EDC0D204A7716E809F0B2DABE207BE7" # # # # # Used as API key from PHP sample steam NEXTAUTH_URL="http://localhost:3000" NEXT_PUBLIC_SERVEREVENTS_WS="ws://localhost:5347" NEXT_PUBLIC_API_BASEPATH="http://localhost:3000/api" WS_NO_BUFFER_UTIL=true # # # # # fix for bug when large text is sent NEXT_PUBLIC_SUPPORT_URL="https://discord.gg/eEjA8ZNm3S" ADMIN_STEAMIDS="76561175373023231"

Expected behavior

https://github.com/Nekonyx/next-auth-steam

Should work as it did before V5.

Nik-Novak commented 2 months ago

Can anyone assist? I've narrowed it down significantly

bigbigbo commented 2 months ago

I encountered the same issue, and upon reviewing the source code, I found that the token.request method was never actually called.

In version 4, the token.request method is called, but in version 5, I only found the userinfo.request method being called.

Nik-Novak commented 2 months ago

I encountered the same issue, and upon reviewing the source code, I found that the token.request method was never actually called.

In version 4, the token.request method is called, but in version 5, I only found the userinfo.request method being called.

I've noticed the same thing.

bigbigbo commented 2 months ago

I encountered the same issue, and upon reviewing the source code, I found that the token.request method was never actually called. In version 4, the token.request method is called, but in version 5, I only found the userinfo.request method being called.

I've noticed the same thing.

In this issue https://github.com/nextauthjs/next-auth/issues/10732#issuecomment-2082463907, the author has responded to the question.

Nik-Novak commented 2 months ago

I encountered the same issue, and upon reviewing the source code, I found that the token.request method was never actually called. In version 4, the token.request method is called, but in version 5, I only found the userinfo.request method being called.

I've noticed the same thing.

In this issue #10732 (comment), the author has responded to the question.

Thanks for coming back to post, renewed my hope lol

smo043 commented 1 month ago

I have the same issue open in Discussions.

https://github.com/nextauthjs/next-auth/discussions/10829

null1s commented 2 days ago

@Nik-Novak

Thanks for coming back to post, renewed my hope lol

Nik, in the end, did you manage to fix this issue? I'm really interested if you did, because I need to use steam provider with authjs v5