Skip to content
Merged
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
2 changes: 2 additions & 0 deletions mpapi-next-gen/docs/dynamo-tables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

mobile-purchases-PROD-user-subscriptions
2 changes: 2 additions & 0 deletions mpapi-next-gen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"validate": "yarn lint:check && yarn format:check"
},
"dependencies": {
"@aws-sdk/client-cloudwatch": "^3.1075.0",
"@aws-sdk/client-dynamodb": "^3.1073.0",
"@aws-sdk/client-s3": "^3.1071.0",
"@aws-sdk/client-sqs": "^3.1073.0",
"@aws-sdk/client-ssm": "^3.1071.0",
"@aws-sdk/lib-dynamodb": "^3.1075.0",
"@aws-sdk/util-dynamodb": "^3.996.5",
"google-auth-library": "^10.7.0"
},
Expand Down
31 changes: 31 additions & 0 deletions mpapi-next-gen/src/common/apiGatewayHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { APIGatewayProxyResult } from 'aws-lambda';

export type QueryParameters = Record<string, string>;

export type PathParameters = Record<string, string | undefined>;

export type HttpRequestHeaders = Record<string, string | undefined>;

export const HTTPResponses: Record<string, APIGatewayProxyResult> = {
OK: { statusCode: 200, body: '{"status": 200, "message": "OK"}' },
INVALID_REQUEST: {
statusCode: 400,
body: '{"status": 400, "message": "INVALID_REQUEST"}',
},
UNAUTHORISED: {
statusCode: 401,
body: '{"status": 401, "message": "UNAUTHORISED"}',
},
FORBIDDEN: {
statusCode: 403,
body: '{"status": 403, "message": "FORBIDDEN"}',
},
NOT_FOUND: {
statusCode: 404,
body: '{"status": 404, "message": "NOT_FOUND"}',
},
INTERNAL_ERROR: {
statusCode: 500,
body: '{"status": 500, "message": "INTERNAL_SERVER_ERROR"}',
},
};
106 changes: 106 additions & 0 deletions mpapi-next-gen/src/common/guIdentityApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { HttpRequestHeaders } from './apiGatewayHttp';
import { Stage } from './appIdentity';
import { getParameterValueUsingAppStageStackConvention } from './ssmParameters';
import OktaJwtVerifier from '@okta/jwt-verifier';

interface OktaJwtVerifierClaims {
scp: [string];
legacy_identity_id: string;
}

interface OktaStageParameters {
issuer: string;
expectedAud: string;
scope: string;
}

export interface UserIdResolution {
status:
| 'incorrect-token'
| 'incorrect-scope'
| 'missing-identity-id'
| 'success';
userId: null | string;
}

export async function getIdentityApiKey(): Promise<string> {
return await getParameterValueUsingAppStageStackConvention<string>(
'mp-soft-opt-in-identity-api-key',
);
}

export async function getMembershipAccountId(): Promise<string> {
return await getParameterValueUsingAppStageStackConvention<string>(
'membershipAccountId',
);
}

export async function getIdentityUrl(): Promise<string> {
return await getParameterValueUsingAppStageStackConvention<string>(
'mp-soft-opt-in-identity-user-consent-domain-url',
);
}

export function getAuthToken(headers: HttpRequestHeaders): string | undefined {
return (headers['Authorization'] ?? headers['authorization'])?.replace(
'Bearer ',
'',
);
}

function getOktaStageParameters(stage: string): OktaStageParameters {
if (stage === 'PROD') {
return {
issuer: 'https://profile.theguardian.com/oauth2/aus3xgj525jYQRowl417',
expectedAud: 'https://profile.theguardian.com/',
scope: 'guardian.mobile-purchases-api.update.self',
};
} else {
return {
issuer:
'https://profile.code.dev-theguardian.com/oauth2/aus3v9gla95Toj0EE0x7',
expectedAud: 'https://profile.code.dev-theguardian.com/',
scope: 'guardian.mobile-purchases-api.update.self',
};
}
}

export async function getUserId(
headers: HttpRequestHeaders,
): Promise<UserIdResolution> {
const oktaparams = getOktaStageParameters(Stage);

const issuer = oktaparams.issuer;
const expectedAud = oktaparams.expectedAud;
const scope = oktaparams.scope;

const oktaJwtVerifier = new OktaJwtVerifier({
issuer: issuer,
});

const accessTokenString = getAuthToken(headers);

try {
return await oktaJwtVerifier
.verifyAccessToken(accessTokenString || '', expectedAud)
.then((payload) => {
const claims = payload.claims as unknown as OktaJwtVerifierClaims;

if (claims.scp.includes(scope)) {
if (claims.legacy_identity_id) {
return {
status: 'success',
userId: claims.legacy_identity_id,
};
} else {
return { status: 'missing-identity-id', userId: null };
}
} else {
return { status: 'incorrect-scope', userId: null };
}
});
} catch (error) {
console.log(`error: ${error}`);
return { status: 'incorrect-token', userId: null };
}
}
11 changes: 11 additions & 0 deletions mpapi-next-gen/src/common/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum Platform {
Ios = 'ios',
Android = 'android',
DailyEdition = 'newsstand',
IosEdition = 'ios-edition',
AndroidEdition = 'android-edition',
IosPuzzles = 'ios-puzzles',
AndroidPuzzles = 'android-puzzles',
IosFeast = 'ios-feast',
AndroidFeast = 'android-feast',
}
15 changes: 15 additions & 0 deletions mpapi-next-gen/src/common/softOptIns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Platform } from './platform';

export type SoftOptInEventProductName = 'InAppPurchase' | 'FeastInAppPurchase';

export const mapPlatformToSoftOptInProductName = (
platform: string | undefined,
): SoftOptInEventProductName => {
switch (platform) {
case Platform.IosFeast:
case Platform.AndroidFeast:
return 'FeastInAppPurchase';
default:
return 'InAppPurchase';
}
};
33 changes: 33 additions & 0 deletions mpapi-next-gen/src/common/ssmParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

const ssmClient = new SSMClient({
region: process.env.AWS_REGION || 'us-east-1',
});

export async function getParameterValue(
parameterName: string,
): Promise<string> {
/*
the input value `parameterName` is expected to be the full name in AWS
for instance:
`/mobile-purchases/${Stage}/google-oauth-lambda/google.serviceAccountJson`
*/

try {
const command = new GetParameterCommand({
Name: parameterName,
WithDecryption: true,
});

const response = await ssmClient.send(command);

if (!response.Parameter?.Value) {
throw new Error('[df35dc74] no credentials found in SSM');
}

return response.Parameter.Value;
} catch (error) {
console.error('[5446b6c6] error retrieving credentials from SSM:', error);
throw error;
}
}
3 changes: 3 additions & 0 deletions mpapi-next-gen/src/common/subscriptionEvent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { App, Stage } from '../common/appIdentity';

// This class abstract the records in the dynamo table
// mobile-purchases-PROD-subscription-events-v2

export class SubscriptionEvent {
subscriptionId: string;
timestampAndType: string;
Expand Down
40 changes: 40 additions & 0 deletions mpapi-next-gen/src/common/userSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DynamoDbTable } from '@aws/dynamodb-data-mapper';
import { App, Stage } from '../common/appIdentity';

// This class abstract the records in the dynamo table
// mobile-purchases-PROD-user-subscriptions

export class UserSubscription {
userId: string;
subscriptionId: string;
creationTimestamp: string;

constructor(
userId: string,
subscriptionId: string,
creationTimestamp: string,
) {
this.userId = userId;
this.subscriptionId = subscriptionId;
this.creationTimestamp = creationTimestamp;
}

static getTableName(): string {
return `${App}-${Stage}-user-subscriptions`;
}

get [DynamoDbTable]() {
return UserSubscription.getTableName();
}
}

// Note:
// UserSubscriptionEmpty is a convenience class for when you need to create
// an empty UserSubscription object. But the type should be used in place
// of UserSubscription.

export class UserSubscriptionEmpty extends UserSubscription {
constructor() {
super('', '', '');
}
}
27 changes: 7 additions & 20 deletions mpapi-next-gen/src/lambdas/googleOauth2.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { APIGatewayProxyEvent } from 'aws-lambda';
import { Stage } from '../common/appIdentity';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { JWT } from 'google-auth-library';
import { getParameterValue } from '../common/ssmParameters';

const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
});

const ssmClient = new SSMClient({
region: process.env.AWS_REGION || 'us-east-1',
});

interface GoogleServiceAccountCredentials {
type: string;
project_id: string;
Expand All @@ -27,23 +23,14 @@ interface GoogleServiceAccountCredentials {

async function getGoogleCredentials(): Promise<GoogleServiceAccountCredentials> {
const parameterName = `/mobile-purchases/${Stage}/google-oauth-lambda/google.serviceAccountJson`;

try {
const command = new GetParameterCommand({
Name: parameterName,
WithDecryption: true,
});

const response = await ssmClient.send(command);

if (!response.Parameter?.Value) {
throw new Error('No credentials found in SSM');
}

// Parse the JSON string from SSM
return JSON.parse(response.Parameter.Value);
const value = await getParameterValue(parameterName);
return JSON.parse(value);
} catch (error) {
console.error('Error retrieving credentials from SSM:', error);
console.error(
'[de62945b] Error retrieving google credentials from SSM:',
error,
);
throw error;
}
}
Expand Down
43 changes: 43 additions & 0 deletions mpapi-next-gen/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@
"@smithy/types" "^4.14.3"
tslib "^2.6.2"

"@aws-sdk/client-cloudwatch@^3.1075.0":
version "3.1075.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.1075.0.tgz#cf3a0a067cc61356ae5daf4c11b1c2ed189915ec"
integrity sha512-Kuc9K+YBw8y43izN5Yz7eE3m3WZ1ea6ynULA94qn0dCd8lX6PKwlG3q7UEKfUr8iDvh02Dz6AdYrGYJFWD8M0g==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "^3.974.23"
"@aws-sdk/credential-provider-node" "^3.972.58"
"@aws-sdk/types" "^3.973.13"
"@smithy/core" "^3.24.6"
"@smithy/fetch-http-handler" "^5.4.6"
"@smithy/middleware-compression" "^4.4.6"
"@smithy/node-http-handler" "^4.7.6"
"@smithy/types" "^4.14.3"
tslib "^2.6.2"

"@aws-sdk/client-dynamodb@^3.1073.0":
version "3.1075.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1075.0.tgz#4a7b10266175da3ef89ec8e019d2478677db417c"
Expand Down Expand Up @@ -295,6 +312,17 @@
mnemonist "0.38.3"
tslib "^2.6.2"

"@aws-sdk/lib-dynamodb@^3.1075.0":
version "3.1075.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.1075.0.tgz#273588fc97cd707cc72947f81b0b371d68763565"
integrity sha512-3OxmMDgk8wvK6QOaVrQuOq9t6xlgS5047aDPxAZxTSwGBcEMc6FtiamtoK3kfClfOt7k3t+NWIo0bIfhWQ4Pzg==
dependencies:
"@aws-sdk/core" "^3.974.23"
"@aws-sdk/util-dynamodb" "^3.996.5"
"@smithy/core" "^3.24.6"
"@smithy/types" "^4.14.3"
tslib "^2.6.2"

"@aws-sdk/middleware-endpoint-discovery@^3.972.19":
version "3.972.19"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.19.tgz#affa05f6ac297a8eff7eeae5650fd08baf16b5f4"
Expand Down Expand Up @@ -895,6 +923,16 @@
dependencies:
tslib "^2.6.2"

"@smithy/middleware-compression@^4.4.6":
version "4.5.2"
resolved "https://registry.yarnpkg.com/@smithy/middleware-compression/-/middleware-compression-4.5.2.tgz#e730ce1ea3b049da99b2f819ddd9603b36725b8e"
integrity sha512-gub8H0OcmWMjuAp94S7IgXV3WaD2psuaiiL2XQtvkrjT7CKJT/cW7v37bNncdiAvUci5a+IiBGn7ugj1qZ7lfw==
dependencies:
"@smithy/core" "^3.26.0"
"@smithy/types" "^4.15.0"
fflate "0.8.1"
tslib "^2.6.2"

"@smithy/node-http-handler@^4.7.6":
version "4.8.2"
resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz#bd7fc4d60220d4629111784e3c5c1d76fa55499f"
Expand Down Expand Up @@ -2126,6 +2164,11 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"

fflate@0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9"
integrity sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==

file-entry-cache@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
Expand Down
Loading