Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions modules/authentication/src/config/apple.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
import { oauth2Schema } from '../constants/index.js';

const appleOAuthClientSchema = {
id: {
doc: 'Unique identifier for this Apple OAuth client credential set',
format: 'String',
default: '',
},
name: {
doc: 'Display name for this Apple OAuth client credential set',
format: 'String',
default: '',
optional: true,
},
clientId: {
format: 'String',
default: '',
},
privateKey: {
doc: 'The private key for this Apple OAuth client',
format: 'String',
default: '',
},
teamId: {
doc: 'The team id for this Apple OAuth client',
format: 'String',
default: '',
},
keyId: {
doc: 'The private key id for this Apple OAuth client',
format: 'String',
default: '',
},
redirect_uri: {
format: 'String',
default: '',
optional: true,
},
};

export default {
apple: {
...oauth2Schema,
clients: {
doc: 'Additional Apple OAuth client credential sets for multi-app support',
format: 'Array',
default: [],
children: appleOAuthClientSchema,
},
privateKey: {
doc: 'The private key that is provided by apple developer console for a specific app',
format: 'String',
Expand Down
44 changes: 31 additions & 13 deletions modules/authentication/src/handlers/oauth2/OAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,22 @@
return (this.initialized = true);
}

protected resolveOAuthClientId(_call: ParsedRouterRequest): string {

Check notice on line 78 in modules/authentication/src/handlers/oauth2/OAuth2.ts

View check run for this annotation

codefactor.io / CodeFactor

modules/authentication/src/handlers/oauth2/OAuth2.ts#L78

'_call' is defined but never used. (@typescript-eslint/no-unused-vars)
return this.settings.clientId;
}

protected getOAuthStateExtras(_call: ParsedRouterRequest): Record<string, unknown> {

Check notice on line 82 in modules/authentication/src/handlers/oauth2/OAuth2.ts

View check run for this annotation

codefactor.io / CodeFactor

modules/authentication/src/handlers/oauth2/OAuth2.ts#L82

'_call' is defined but never used. (@typescript-eslint/no-unused-vars)
return {};
}

async initNative(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const scopes = call.request.params?.scopes ?? this.defaultScopes;
const { anonymousUser } = call.request.context;
const conduitUrl = (await this.grpcSdk.config.get('router')).hostUrl;

// returns part of regular redirect options for native usage
const queryOptions: Partial<RedirectOptions> = {
client_id: this.settings.clientId,
client_id: this.resolveOAuthClientId(call),
redirect_uri: conduitUrl + this.settings.callbackUrl,
scope: this.constructScopes(scopes),
};
Expand All @@ -97,6 +105,7 @@
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
...this.getOAuthStateExtras(call),
},
})
.catch(err => {
Expand Down Expand Up @@ -125,7 +134,7 @@
.replace(/\//g, '_');
}
const queryOptions: RedirectOptions = {
client_id: this.settings.clientId,
client_id: this.resolveOAuthClientId(call),
redirect_uri: conduitUrl + this.settings.callbackUrl,
response_type: this.settings.responseType,
response_mode: this.settings.responseMode,
Expand All @@ -150,6 +159,7 @@
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
...this.getOAuthStateExtras(call),
},
})
.catch(err => {
Expand Down Expand Up @@ -360,12 +370,7 @@
path: `/init/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} authentication.`,
action: ConduitRouteActions.GET,
queryParams: {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
redirectUri: ConduitString.Optional,
},
queryParams: this.getInitRouteQueryParams(),
middlewares: initRouteMiddleware,
rateLimit: OAUTH_INIT,
},
Expand All @@ -381,11 +386,7 @@
path: `/initNative/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} native authentication.`,
action: ConduitRouteActions.GET,
queryParams: {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
},
queryParams: this.getInitNativeRouteQueryParams(),
middlewares: initRouteMiddleware,
rateLimit: OAUTH_INIT,
},
Expand Down Expand Up @@ -455,6 +456,23 @@
}
}

protected getInitRouteQueryParams() {
return {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
redirectUri: ConduitString.Optional,
};
}

protected getInitNativeRouteQueryParams() {
return {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
};
}

makeRequest(data: AuthParams): OAuthRequest {
return {
method: this.settings.accessTokenMethod,
Expand Down
111 changes: 61 additions & 50 deletions modules/authentication/src/handlers/oauth2/apple/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@conduitplatform/grpc-sdk';
import appleParameters from '../apple/apple.json' with { type: 'json' };
import {
AppleClientCredentials,
AppleOAuth2Settings,
AppleProviderConfig,
ConnectionParams,
Expand All @@ -22,8 +23,7 @@ import { Token } from '../../../models/index.js';
import { status } from '@grpc/grpc-js';
import moment from 'moment';
import jwksRsa from 'jwks-rsa';

import { validateStateToken } from '../utils/index.js';
import { validateStateToken, resolveAppleOAuthClient } from '../utils/index.js';
import {
ConduitJson,
ConduitString,
Expand All @@ -49,7 +49,8 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
!authConfig['apple'] ||
!authConfig['apple'].clientId ||
!authConfig['apple'].privateKey ||
!authConfig['apple'].teamId
!authConfig['apple'].teamId ||
!authConfig['apple'].keyId
) {
ConduitGrpcSdk.Logger.log(`Apple authentication not available`);
return (this.initialized = false);
Expand All @@ -66,43 +67,47 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
return scopes.join(' ');
}

protected getInitRouteQueryParams() {
return {
...super.getInitRouteQueryParams(),
oauthClientId: ConduitString.Optional,
};
}

protected getInitNativeRouteQueryParams() {
return {
...super.getInitNativeRouteQueryParams(),
oauthClientId: ConduitString.Optional,
};
}

protected resolveOAuthClientId(call: ParsedRouterRequest): string {
return resolveAppleOAuthClient(call.request.params?.oauthClientId).clientId;
}

protected getOAuthStateExtras(call: ParsedRouterRequest): Record<string, unknown> {
return { oauthClientId: call.request.params?.oauthClientId };
}

async authorize(call: ParsedRouterRequest) {
const params = call.request.params;
const stateToken = await validateStateToken(params.state);
const providerClient = resolveAppleOAuthClient(stateToken.data.oauthClientId);
const decoded_id_token = jwt.decode(params.id_token, { complete: true });

const publicKeys = await axios.get('https://appleid.apple.com/auth/keys');
const publicKey = publicKeys.data.keys.find(
(key: Indexable) => key.kid === decoded_id_token!.header.kid,
);
const applePublicKey = await this.generateApplePublicKey(publicKey.kid);
this.verifyIdentityToken(applePublicKey, params.id_token);
this.verifyIdentityToken(applePublicKey, params.id_token, providerClient.clientId);

const apple_private_key = this.settings.privateKey;
const apple_client_secret = this.buildAppleClientSecret(providerClient);

const jwtHeader = {
alg: 'ES256',
kid: this.settings.keyId,
};

const jwtPayload = {
iss: this.settings.teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
aud: 'https://appleid.apple.com',
sub: this.settings.clientId,
};

const apple_client_secret = jwt.sign(jwtPayload, apple_private_key, {
algorithm: 'ES256',
header: jwtHeader,
});

const clientId = this.settings.clientId;
const conduitUrl = (await this.grpcSdk.config.get('router')).hostUrl;
const config = ConfigController.getInstance().config;
const tokenBody = new URLSearchParams();
tokenBody.set('client_id', clientId);
tokenBody.set('client_id', providerClient.clientId);
tokenBody.set('client_secret', apple_client_secret);
tokenBody.set('code', params.code);
if (this.settings.grantType) {
Expand Down Expand Up @@ -148,7 +153,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {

const redirectUri =
AuthUtils.validateRedirectUri(stateToken.data.customRedirectUri) ??
this.settings.finalRedirect;
providerClient.redirect_uri;
const conduitClientId = stateToken.data.clientId;

return TokenProvider.getInstance()!.provideUserTokens(
Expand All @@ -164,39 +169,21 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
async nativeAuthorize(call: ParsedRouterRequest) {
const params = call.request.bodyParams;
const stateToken = await validateStateToken(params.state);
const providerClient = resolveAppleOAuthClient(stateToken.data.oauthClientId);
const decoded_id_token = jwt.decode(params.id_token, { complete: true });

const publicKeys = await axios.get('https://appleid.apple.com/auth/keys');
const publicKey = publicKeys.data.keys.find(
(key: Indexable) => key.kid === decoded_id_token!.header.kid,
);
const applePublicKey = await this.generateApplePublicKey(publicKey.kid);
this.verifyIdentityToken(applePublicKey, params.id_token);

const apple_private_key = this.settings.privateKey;

const jwtHeader = {
alg: 'ES256',
kid: this.settings.keyId,
};

const jwtPayload = {
iss: this.settings.teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
aud: 'https://appleid.apple.com',
sub: this.settings.clientId,
};
this.verifyIdentityToken(applePublicKey, params.id_token, providerClient.clientId);

const apple_client_secret = jwt.sign(jwtPayload, apple_private_key, {
algorithm: 'ES256',
header: jwtHeader,
});
const apple_client_secret = this.buildAppleClientSecret(providerClient);

const clientId = this.settings.clientId;
const config = ConfigController.getInstance().config;
const nativeTokenBody = new URLSearchParams();
nativeTokenBody.set('client_id', clientId);
nativeTokenBody.set('client_id', providerClient.clientId);
nativeTokenBody.set('client_secret', apple_client_secret);
nativeTokenBody.set('code', params.code);
if (this.settings.grantType) {
Expand Down Expand Up @@ -277,7 +264,31 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
return key.getPublicKey();
}

private verifyIdentityToken(applePublicKey: string, id_token: string) {
private buildAppleClientSecret(providerClient: AppleClientCredentials): string {
const jwtHeader = {
alg: 'ES256',
kid: providerClient.keyId,
};

const jwtPayload = {
iss: providerClient.teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
aud: 'https://appleid.apple.com',
sub: providerClient.clientId,
};

return jwt.sign(jwtPayload, providerClient.privateKey, {
algorithm: 'ES256',
header: jwtHeader,
});
}

private verifyIdentityToken(
applePublicKey: string,
id_token: string,
expectedClientId: string,
) {
const decoded = jwt.decode(id_token, { complete: true }) as Jwt;
const payload = decoded.payload as JwtPayload;
const header = decoded.header as JwtHeader;
Expand All @@ -291,7 +302,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
if (payload.iss !== 'https://appleid.apple.com') {
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid iss');
}
if (payload.aud !== this.settings.clientId) {
if (payload.aud !== expectedClientId) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid aud');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { ProviderConfig } from './ProviderConfig.js';

export interface AppleClientCredentials {
clientId: string;
privateKey: string;
teamId: string;
keyId: string;
}

export interface AppleOAuthClientConfig {
id: string;
name?: string;
clientId: string;
privateKey?: string;
teamId?: string;
keyId?: string;
redirect_uri?: string;
}

export interface ResolvedAppleOAuthClient extends AppleClientCredentials {
redirect_uri: string;
}

export interface AppleProviderConfig extends ProviderConfig {
privateKey: string;
teamId: string;
keyId: string;
clients?: AppleOAuthClientConfig[];
}
1 change: 1 addition & 0 deletions modules/authentication/src/handlers/oauth2/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ValidateStateToken.js';
export * from './MakeRequest.js';
export * from './resolveAppleOAuthClient.js';
Loading