Skip to content

[pull] master from kiwicom:master #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist/*
.nyc_output

10 changes: 3 additions & 7 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint"
],
"extends": ["plugin:@typescript-eslint/recommended"],
"rules": {
"@typescript-eslint/array-type": ["error", "array-simple"],
"@typescript-eslint/explicit-member-accessibility": ["off"],
"@typescript-eslint/no-non-null-assertion": ["off"],
"@typescript-eslint/no-use-before-define": ["off"],
Expand All @@ -27,6 +22,7 @@
"allowExpressions": true,
"allowHigherOrderFunctions": true
}
]
],
"@typescript-eslint/indent": ["off"]
}
}
6 changes: 3 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:

strategy:
matrix:
node-version: [10.x, 12.x]
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: yarn install, build, and test
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Use Node.js 12.x
uses: actions/setup-node@v1
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 12.x
node-version: 16.x
- name: Release
run: |
yarn
yarn release
env:
CI: true
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
.nyc_output
.nyc_output
.idea
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/*
.nyc_output
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Contributing to IAM middleware package

## Table of Contents
1. [Commit style](#commit-style)

1. [Commit style](#commit-style)
1. [Code Formatting](#code-formatting)

### Commit Style

Commits need to adhere to semantic release format in order to release/not release new versions correctly.
Commits need to adhere to semantic release format in order to release/not release new versions correctly.
Please check https://semantic-release.gitbook.io/semantic-release/#commit-message-format.

### Code Formatting

In CI, `yarn prettier:check` verifies the formatting of the code. To ensure your code is formatted correctly, please
run `yarn fmt` before committing it.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ CLIENT_ID - OAuth Client ID for authenticating from a desktop app
CLIENT_SECRET - OAuth Client secret for authenticating from a desktop app
```

**These are optional environment variables:**

```
GOOGLE_REDIRECT_URI - local server uri, which uses google OAuth for redirecting (default 'http://localhost:3000')
OAUTH_SERVER_PORT - your local server port (default '3000')
```

`package.json`

```json
Expand All @@ -150,7 +157,7 @@ CLIENT_SECRET - OAuth Client secret for authenticating from a desktop app
}
```

Now run `generate:token` a browser will open displaying an authorization code (starting with `4/`) and the CLI will ask you to input the authorization code. After that you will be provided with a `refresh_token` (starting with `1/`) that has long validity and can be used for local development.
Now run `generate:token`. This script will start local server (on default port 3000) and the server will automatically open your browser with Google's OAuth web page for your application. After clicking on the button, Google will redirect you back to your GOOGLE_REDIRECT_URI (which defaults to http://localhost:3000) and you will be provided with a `refresh_token` (starting with `1/`) that has long validity and can be used for local development.

# Contributing

Expand Down
45 changes: 24 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,40 @@
"scripts": {
"test": "nyc ava",
"lint": "eslint . --ext ts",
"ci": "npm run test && npm run lint && tsc --noEmit",
"build": "rm -rf dist && tsc --declaration",
"prettier:check": "prettier . --check",
"fmt": "prettier . --write",
"ci": "npm run test && npm run lint && npm run prettier:check && tsc",
"build": "rm -rf dist && tsc --project ./tsconfig.prod.json",
"prepare": "npm run build",
"release": "semantic-release"
},
"dependencies": {
"@types/express": "^4.17.2",
"express": "^4.17.1",
"graphql": "^14.4.2",
"graphql-tools": "^4.0.5",
"@types/express": "^4.17.14",
"express": "^4.18.2",
"graphql": "^15.0.0",
"graphql-tools": "^7.0.5",
"jsonwebtoken": "^8.5.1",
"mamushi": "^2.0.0",
"node-fetch": "^2.6.0",
"open": "^7.0.0"
"open": "^8.4.0",
"url": "^0.11.0"
},
"devDependencies": {
"@semantic-release/commit-analyzer": "8.0.1",
"@semantic-release/github": "7.0.5",
"@semantic-release/npm": "7.0.5",
"@types/graphql": "14.2.3",
"@types/jsonwebtoken": "8.3.7",
"@types/node-fetch": "2.5.5",
"@typescript-eslint/eslint-plugin": "1.13.0",
"@typescript-eslint/parser": "1.13.0",
"@semantic-release/commit-analyzer": "9.0.2",
"@semantic-release/github": "8.0.6",
"@semantic-release/npm": "9.0.1",
"@types/graphql": "14.5.0",
"@types/jsonwebtoken": "8.5.9",
"@types/node-fetch": "2.6.2",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"ava": "2.4.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.10.1",
"nyc": "15.0.0",
"semantic-release": "17.0.4",
"ts-node": "8.6.2",
"typescript": "3.7.5"
"eslint": "8.28.0",
"nyc": "15.1.0",
"prettier": "^2.8.0",
"semantic-release": "19.0.5",
"ts-node": "10.9.1",
"typescript": "4.9.3"
},
"resolutions": {
"mem": "4.0.0"
Expand Down
4 changes: 1 addition & 3 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"extends": [
"config:base"
]
"extends": ["config:base"]
}
48 changes: 27 additions & 21 deletions src/authenticationMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import test from "ava";
import { audienceRegex, authenticationMiddleware, required, validateAudience } from "./authenticationMiddleware";
import {
audienceRegex,
authenticationMiddleware,
required,
validateAudience,
} from "./authenticationMiddleware";

test("required returns correct values", (t) => {
const tests: Array<[any, string]> = [
Expand All @@ -22,69 +27,70 @@ test("validateAudience passes on valid values", (t) => {
"/projects/000000000000/global/backendServices/0000000000000000000",
"/projects/000000000000/apps/my-sample-project-191923",
];
tests.forEach(audience => {
tests.forEach((audience) => {
t.notThrows(() => validateAudience(audience));
});
});

test("validateAudience throws on invalid values", (t) => {
const tests: any[] = [
"not a valid audience",
"",
undefined,
{},
];
tests.forEach(audience => {
const tests: any[] = ["not a valid audience", "", undefined, {}];
tests.forEach((audience) => {
const err = t.throws(() => validateAudience(audience));
t.is(err.message, `'audience' needs to be a string matching ${audienceRegex}`);
t.is(
err?.message,
`'audience' needs to be a string matching ${audienceRegex}`,
);
});
});

test("required throws on null or undefined", (t) => {
const err = t.throws(() => required(undefined, "notExisting"));
t.is(err.message, "Missing 'notExisting', option must be specified.");
t.is(err?.message, "Missing 'notExisting', option must be specified.");

const err2 = t.throws(() => required(null, "notExisting"));
t.is(err2.message, "Missing 'notExisting', option must be specified.");
t.is(err2?.message, "Missing 'notExisting', option must be specified.");
});

test("authenticationMiddleware throws on getting IAP token if initialised with audience", async t => {
test("authenticationMiddleware throws on getting IAP token if initialised with audience", async (t) => {
const tests: string[] = [
"/projects/000000000000/global/backendServices/0000000000000000000",
"/projects/000000000000/apps/my-sample-project-191923",
];

for (let audience of tests) {
for (const audience of tests) {
const opts = {
audience: audience
audience: audience,
};

await t.throwsAsync(
() => authenticationMiddleware(opts)({} as any, {} as any, {} as any),
{instanceOf: TypeError, message: "req.get is not a function"}
{ instanceOf: TypeError, message: "req.get is not a function" },
);
}
});

test("authenticationMiddleware throws on malformed audience", async t => {
test("authenticationMiddleware throws on malformed audience", async (t) => {
const opts = {
audience: "not a valid audience"
audience: "not a valid audience",
};

await t.throwsAsync(
() => authenticationMiddleware(opts)({} as any, {} as any, {} as any),
{instanceOf: TypeError, message: `'audience' needs to be a string matching ${audienceRegex}`}
{
instanceOf: TypeError,
message: `'audience' needs to be a string matching ${audienceRegex}`,
},
);
});

test("authenticationMiddleware throws on getting IAP token if initialised with deprecated options", async t => {
test("authenticationMiddleware throws on getting IAP token if initialised with deprecated options", async (t) => {
const opts = {
iapProjectNumber: "test",
iapServiceID: "test",
};

await t.throwsAsync(
() => authenticationMiddleware(opts)({} as any, {} as any, {} as any),
{instanceOf: TypeError, message: "req.get is not a function"}
{ instanceOf: TypeError, message: "req.get is not a function" },
);
});
70 changes: 37 additions & 33 deletions src/authenticationMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Request, Response, NextFunction } from "express";
import { validateIAPToken } from "./validateIAPToken";

export const audienceRegex = /^\/projects\/\d+\/(?:apps|global\/backendServices)\/(?:\d+|[a-z0-9-]+)$/;
export const audienceRegex =
/^\/projects\/\d+\/(?:apps|global\/backendServices)\/(?:\d+|[a-z0-9-]+)$/;

interface Options {
audience?: string;
Expand All @@ -18,40 +19,43 @@ export function required<T>(value: T | undefined | null, fieldName: string): T {
}

export function validateAudience(audience: string | undefined): void {
if (!(typeof audience === 'string' && audience.match(audienceRegex))) {
throw TypeError(`'audience' needs to be a string matching ${audienceRegex}`);
if (!(typeof audience === "string" && audience.match(audienceRegex))) {
throw TypeError(
`'audience' needs to be a string matching ${audienceRegex}`,
);
}
}

export const authenticationMiddleware = (opts: Options) => async (
req: Request,
_: Response,
next: NextFunction,
) => {
// Handle deprecated options
if (opts.iapProjectNumber && opts.iapServiceID) {
required(opts.iapProjectNumber, "iapProjectNumber");
required(opts.iapServiceID, "iapServiceID");

console.log("'iapProjectNumber' and 'iapServiceID' are deprecated and will be removed. Use 'audience' instead.");
}
else {
validateAudience(required(opts.audience, "audience"));
}

const expectedAudience = opts.audience || `/projects/${opts.iapProjectNumber}/global/backendServices/${opts.iapServiceID}`;
const iapToken = req.get("x-goog-iap-jwt-assertion");

try {
if (!iapToken) {
throw Error("IAP token not present");
export const authenticationMiddleware =
(opts: Options) => async (req: Request, _: Response, next: NextFunction) => {
// Handle deprecated options
if (opts.iapProjectNumber && opts.iapServiceID) {
required(opts.iapProjectNumber, "iapProjectNumber");
required(opts.iapServiceID, "iapServiceID");

console.log(
"'iapProjectNumber' and 'iapServiceID' are deprecated and will be removed. Use 'audience' instead.",
);
} else {
validateAudience(required(opts.audience, "audience"));
}

await validateIAPToken(iapToken, expectedAudience);
next();
} catch (err) {
console.log("IAP validation failed", err);
err.status = 403;
next(err);
}
};
const expectedAudience =
opts.audience ||
`/projects/${opts.iapProjectNumber}/global/backendServices/${opts.iapServiceID}`;
const iapToken = req.get("x-goog-iap-jwt-assertion");

try {
if (!iapToken) {
throw Error("IAP token not present");
}

await validateIAPToken(iapToken, expectedAudience);
next();
} catch (err: any) {
// TODO: improve typing for err
console.log("IAP validation failed", err);
err.status = 403;
next(err);
}
};
Loading