sourcefuse / loopback4-authentication

A loopback-next extension for authentication feature. Oauth strategies supported.
https://www.npmjs.com/package/loopback4-authentication
MIT License
73 stars 33 forks source link

Passport-apple is giving an empty profile back #199

Closed apfz closed 6 months ago

apfz commented 10 months ago

Describe the bug When adding passport-apple I am receiving an empty profile response.

To Reproduce I've implemented a custom apple oauth provider in apple-oauth2-verify.provider.ts: (NOTE: the docs show a wrong return function. idToken is returned as 3rd argument, not the profile)

import {Provider} from '@loopback/context';
import {repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {AuthErrorKeys, VerifyFunction} from 'loopback4-authentication';
import * as AppleStrategy from 'passport-apple';

import {Tenant} from '../models';
import {UserCredentialsRepository, UserRepository} from '../repositories';
import {AuthUser} from '../models/auth-user.model';

export class AppleOauth2VerifyProvider implements Provider<VerifyFunction.AppleAuthFn> {
  constructor(
    @repository(UserRepository)
    public userRepository: UserRepository,
    @repository(UserCredentialsRepository)
    public userCredsRepository: UserCredentialsRepository,
  ) {}

  value() {
    return async (accessToken: string, refreshToken: string, decodedIdToken: string, profile: AppleStrategy.Profile) => {
      const user = await this.userRepository.findOne({
        where: {
          /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
          email: (profile as any)._json.email,
        },
      });
      if (!user) {
        throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
      }
      const creds = await this.userCredsRepository.findOne({
        where: {
          userId: user.id,
        },
      });
      if (!creds || creds.authProvider !== 'apple' || creds.authId !== profile.id) {
        throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
      }

      const authUser: AuthUser = new AuthUser(user);
      authUser.permissions = [];
      authUser.externalAuthToken = accessToken;
      authUser.externalRefreshToken = refreshToken;
      authUser.tenant = new Tenant({id: user.defaultTenant});
      return authUser;
    };
  }
}

Bound it in application.ts:

this.bind(Strategies.Passport.APPLE_OAUTH2_VERIFIER).toProvider(AppleOauth2VerifyProvider);

and added the endpoints in login.controller:

@authenticateClient(STRATEGY.CLIENT_PASSWORD)
  @authenticate(
    STRATEGY.APPLE_OAUTH2,
    {
      accessType: 'offline',
      scope: ['name', 'email'],
      callbackURL: process.env.APPLE_AUTH_CALLBACK_URL,
      clientID: process.env.APPLE_AUTH_CLIENT_ID,
      teamID: process.env.APPLE_AUTH_TEAM_ID,
      keyID: process.env.APPLE_AUTH_KEY_ID,
      privateKeyLocation: process.env.APPLE_AUTH_PRIVATE_KEY_LOCATION,
    },
    (req: Request) => {
      return {
        accessType: 'offline',
        state: Object.keys(req.query)
          .map((key) => key + '=' + req.query[key])
          .join('&'),
      };
    },
  )
  @authorize({permissions: ['*']})
  @get('/auth/apple', {
    responses: {
      [STATUS_CODE.OK]: {
        description: 'Token Response',
        content: {
          [CONTENT_TYPE.JSON]: {
            schema: {'x-ts-type': TokenResponse},
          },
        },
      },
    },
  })
  async loginViaApple(
    @param.query.string('client_id')
    clientId?: string,
    @param.query.string('client_secret')
    clientSecret?: string,
  ): Promise<void> {}

  @authenticate(
    STRATEGY.APPLE_OAUTH2,
    {
      accessType: 'offline',
      scope: ['name', 'email'],
      callbackURL: process.env.APPLE_AUTH_CALLBACK_URL,
      clientID: process.env.APPLE_AUTH_CLIENT_ID,
      teamID: process.env.APPLE_AUTH_TEAM_ID,
      keyID: process.env.APPLE_AUTH_KEY_ID,
      privateKeyLocation: process.env.APPLE_AUTH_PRIVATE_KEY_LOCATION,
    },
    (req: Request) => {
      return {
        accessType: 'offline',
        state: Object.keys(req.query)
          .map((key) => `${key}=${req.query[key]}`)
          .join('&'),
      };
    },
  )
  @authorize({permissions: ['*']})
  @post('/auth/apple-auth-redirect', {
    responses: {
      [STATUS_CODE.OK]: {
        description: 'Token Response',
        content: {
          [CONTENT_TYPE.FORM_DATA]: {
            schema: {'x-ts-type': TokenResponse},
          },
        },
      },
    },
  })
  async appleCallback(
    @requestBody({
      content: {
        'application/x-www-form-urlencoded': {
          schema: { type: 'object' },
        }
      }
    }) requestData: string,
    @inject(RestBindings.Http.RESPONSE) response: Response,
  ): Promise<void> {
    const clientId = new URLSearchParams(requestData).get('client_id');
    if (!clientId || !this.user) {
      throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
    }
    const client = await this.authClientRepository.findOne({
      where: {
        clientId: clientId,
      },
    });
    if (!client?.redirectUrl) {
      throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
    }
    try {
      const codePayload: ClientAuthCode<User> = {
        clientId,
        user: this.user,
      };
      const token = jwt.sign(codePayload, client.secret, {
        expiresIn: client.authCodeExpiration,
        audience: clientId,
        subject: this.user.username,
        issuer: process.env.JWT_ISSUER,
      });
      response.redirect(`${client.redirectUrl}?code=${token}`);
    } catch (error) {
      throw new HttpErrors.InternalServerError(AuthErrorKeys.UnknownError);
    }
  }

Expected behavior In the apple-oauth2-verify.provider.ts file I expected the profile to contain the user profile information.

I do get the code, state and user object successfully back from Apple but it is not being recognized. Do I need to implement this differently?

stale[bot] commented 6 months ago

This issue has been marked stale because it has not seen any activity within three months. If you believe this to be an error, please contact one of the code owners. This issue will be closed within 15 days of being stale.

stale[bot] commented 6 months ago

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners.