loopback4-authentication
loopback4-authentication copied to clipboard
Passport-apple is giving an empty profile back
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?