feat(graphql): Add OpenID Connect flow queries.

This commit is contained in:
SamTolmay 2021-03-03 13:14:54 +02:00
parent bd7a7720f5
commit 1ac0b3d318
36 changed files with 834 additions and 10 deletions

225
.pnp.cjs generated
View File

@ -4453,9 +4453,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["graphql", "npm:15.5.0"],
["graphql-type-json", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:0.3.2"],
["jest", "npm:26.6.3"],
["jsonwebtoken", "npm:8.5.1"],
["mingo", "npm:4.1.2"],
["moment", "npm:2.29.1"],
["mongodb", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:3.6.4"],
["openid-client", "npm:4.4.1"],
["saslprep", "npm:1.0.3"],
["webpack", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:5.22.0"],
["webpack-cli", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:4.5.0"]
@ -4977,6 +4979,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["@panva/asn1.js", [
["npm:1.0.0", {
"packageLocation": "./.yarn/cache/@panva-asn1.js-npm-1.0.0-2bf51df722-0563c1372d.zip/node_modules/@panva/asn1.js/",
"packageDependencies": [
["@panva/asn1.js", "npm:1.0.0"]
],
"linkType": "HARD",
}]
]],
["@protobufjs/aspromise", [
["npm:1.1.2", {
"packageLocation": "./.yarn/cache/@protobufjs-aspromise-npm-1.1.2-71d00b938f-83ced0798a.zip/node_modules/@protobufjs/aspromise/",
@ -7220,6 +7231,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["aggregate-error", [
["npm:3.1.0", {
"packageLocation": "./.yarn/cache/aggregate-error-npm-3.1.0-415a406f4e-704d2001a3.zip/node_modules/aggregate-error/",
"packageDependencies": [
["aggregate-error", "npm:3.1.0"],
["clean-stack", "npm:2.2.0"],
["indent-string", "npm:4.0.0"]
],
"linkType": "HARD",
}]
]],
["airbnb-prop-types", [
["npm:2.16.0", {
"packageLocation": "./.yarn/cache/airbnb-prop-types-npm-2.16.0-d794f5271c-41b34cf2d2.zip/node_modules/airbnb-prop-types/",
@ -9807,6 +9829,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["clean-stack", [
["npm:2.2.0", {
"packageLocation": "./.yarn/cache/clean-stack-npm-2.2.0-a8ce435a5c-e291ce2b8c.zip/node_modules/clean-stack/",
"packageDependencies": [
["clean-stack", "npm:2.2.0"]
],
"linkType": "HARD",
}]
]],
["clean-webpack-plugin", [
["npm:3.0.0", {
"packageLocation": "./.yarn/cache/clean-webpack-plugin-npm-3.0.0-21f4eeb4fb-fc0fbd1c8e.zip/node_modules/clean-webpack-plugin/",
@ -14487,6 +14518,24 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],
"linkType": "HARD",
}],
["npm:11.8.2", {
"packageLocation": "./.yarn/cache/got-npm-11.8.2-c1eb105458-6415f98ec2.zip/node_modules/got/",
"packageDependencies": [
["got", "npm:11.8.2"],
["@sindresorhus/is", "npm:4.0.0"],
["@szmarczak/http-timer", "npm:4.0.5"],
["@types/cacheable-request", "npm:6.0.1"],
["@types/responselike", "npm:1.0.0"],
["cacheable-lookup", "npm:5.0.4"],
["cacheable-request", "npm:7.0.1"],
["decompress-response", "npm:6.0.0"],
["http2-wrapper", "npm:1.0.0-beta.5.2"],
["lowercase-keys", "npm:2.0.0"],
["p-cancelable", "npm:2.0.0"],
["responselike", "npm:2.0.0"]
],
"linkType": "HARD",
}],
["npm:9.6.0", {
"packageLocation": "./.yarn/cache/got-npm-9.6.0-80edc15fd0-4cfb862eb7.zip/node_modules/got/",
"packageDependencies": [
@ -17144,6 +17193,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["jose", [
["npm:2.0.4", {
"packageLocation": "./.yarn/cache/jose-npm-2.0.4-8e257b3aaa-402054b230.zip/node_modules/jose/",
"packageDependencies": [
["jose", "npm:2.0.4"],
["@panva/asn1.js", "npm:1.0.0"]
],
"linkType": "HARD",
}]
]],
["js-tokens", [
["npm:4.0.0", {
"packageLocation": "./.yarn/cache/js-tokens-npm-4.0.0-0ac852e9e2-1fc4e4667a.zip/node_modules/js-tokens/",
@ -17411,6 +17470,25 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["jsonwebtoken", [
["npm:8.5.1", {
"packageLocation": "./.yarn/cache/jsonwebtoken-npm-8.5.1-c007670b76-ea44bbb7a7.zip/node_modules/jsonwebtoken/",
"packageDependencies": [
["jsonwebtoken", "npm:8.5.1"],
["jws", "npm:3.2.2"],
["lodash.includes", "npm:4.3.0"],
["lodash.isboolean", "npm:3.0.3"],
["lodash.isinteger", "npm:4.0.4"],
["lodash.isnumber", "npm:3.0.3"],
["lodash.isplainobject", "npm:4.0.6"],
["lodash.isstring", "npm:4.0.1"],
["lodash.once", "npm:4.1.1"],
["ms", "npm:2.1.3"],
["semver", "npm:5.7.1"]
],
"linkType": "HARD",
}]
]],
["jsprim", [
["npm:1.4.1", {
"packageLocation": "./.yarn/cache/jsprim-npm-1.4.1-948d2c9ec3-ee0177b7ef.zip/node_modules/jsprim/",
@ -17436,6 +17514,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]
]],
["jwa", [
["npm:1.4.1", {
"packageLocation": "./.yarn/cache/jwa-npm-1.4.1-4f19d6572c-e3a6234a3a.zip/node_modules/jwa/",
"packageDependencies": [
["jwa", "npm:1.4.1"],
["buffer-equal-constant-time", "npm:1.0.1"],
["ecdsa-sig-formatter", "npm:1.0.11"],
["safe-buffer", "npm:5.2.1"]
],
"linkType": "HARD",
}],
["npm:2.0.0", {
"packageLocation": "./.yarn/cache/jwa-npm-2.0.0-52a7c3f1ca-03f8af36a3.zip/node_modules/jwa/",
"packageDependencies": [
@ -17448,6 +17536,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]
]],
["jws", [
["npm:3.2.2", {
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-3990b26ebb.zip/node_modules/jws/",
"packageDependencies": [
["jws", "npm:3.2.2"],
["jwa", "npm:1.4.1"],
["safe-buffer", "npm:5.2.1"]
],
"linkType": "HARD",
}],
["npm:4.0.0", {
"packageLocation": "./.yarn/cache/jws-npm-4.0.0-2a24fd53b9-fb7d2725da.zip/node_modules/jws/",
"packageDependencies": [
@ -17898,6 +17995,24 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["lodash.includes", [
["npm:4.3.0", {
"packageLocation": "./.yarn/cache/lodash.includes-npm-4.3.0-3a2f6fa22c-20d6b1bf78.zip/node_modules/lodash.includes/",
"packageDependencies": [
["lodash.includes", "npm:4.3.0"]
],
"linkType": "HARD",
}]
]],
["lodash.isboolean", [
["npm:3.0.3", {
"packageLocation": "./.yarn/cache/lodash.isboolean-npm-3.0.3-b575b41488-e5b7a921f4.zip/node_modules/lodash.isboolean/",
"packageDependencies": [
["lodash.isboolean", "npm:3.0.3"]
],
"linkType": "HARD",
}]
]],
["lodash.isequal", [
["npm:4.5.0", {
"packageLocation": "./.yarn/cache/lodash.isequal-npm-4.5.0-f8b0f64d63-5b47e09464.zip/node_modules/lodash.isequal/",
@ -17907,6 +18022,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["lodash.isinteger", [
["npm:4.0.4", {
"packageLocation": "./.yarn/cache/lodash.isinteger-npm-4.0.4-42add9f4e1-a29551cc9a.zip/node_modules/lodash.isinteger/",
"packageDependencies": [
["lodash.isinteger", "npm:4.0.4"]
],
"linkType": "HARD",
}]
]],
["lodash.ismatch", [
["npm:4.4.0", {
"packageLocation": "./.yarn/cache/lodash.ismatch-npm-4.4.0-e538fd6c3d-f6e3ef9fd3.zip/node_modules/lodash.ismatch/",
@ -17916,6 +18040,33 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["lodash.isnumber", [
["npm:3.0.3", {
"packageLocation": "./.yarn/cache/lodash.isnumber-npm-3.0.3-b3bb5f7347-a33b10bf57.zip/node_modules/lodash.isnumber/",
"packageDependencies": [
["lodash.isnumber", "npm:3.0.3"]
],
"linkType": "HARD",
}]
]],
["lodash.isplainobject", [
["npm:4.0.6", {
"packageLocation": "./.yarn/cache/lodash.isplainobject-npm-4.0.6-d73937742f-72a114b610.zip/node_modules/lodash.isplainobject/",
"packageDependencies": [
["lodash.isplainobject", "npm:4.0.6"]
],
"linkType": "HARD",
}]
]],
["lodash.isstring", [
["npm:4.0.1", {
"packageLocation": "./.yarn/cache/lodash.isstring-npm-4.0.1-721fee791c-20c46960b7.zip/node_modules/lodash.isstring/",
"packageDependencies": [
["lodash.isstring", "npm:4.0.1"]
],
"linkType": "HARD",
}]
]],
["lodash.merge", [
["npm:4.6.2", {
"packageLocation": "./.yarn/cache/lodash.merge-npm-4.6.2-77cb4416bf-4e2bb42a87.zip/node_modules/lodash.merge/",
@ -17925,6 +18076,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["lodash.once", [
["npm:4.1.1", {
"packageLocation": "./.yarn/cache/lodash.once-npm-4.1.1-d8ba329ead-236e00ca5f.zip/node_modules/lodash.once/",
"packageDependencies": [
["lodash.once", "npm:4.1.1"]
],
"linkType": "HARD",
}]
]],
["lodash.set", [
["npm:4.3.2", {
"packageLocation": "./.yarn/cache/lodash.set-npm-4.3.2-7586c942c2-4dfedacae1.zip/node_modules/lodash.set/",
@ -18186,6 +18346,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["make-error", [
["npm:1.3.6", {
"packageLocation": "./.yarn/cache/make-error-npm-1.3.6-ccb85d9458-2c780bab84.zip/node_modules/make-error/",
"packageDependencies": [
["make-error", "npm:1.3.6"]
],
"linkType": "HARD",
}]
]],
["make-fetch-happen", [
["npm:5.0.2", {
"packageLocation": "./.yarn/cache/make-fetch-happen-npm-5.0.2-4da3d5b759-7d3a954422.zip/node_modules/make-fetch-happen/",
@ -19719,6 +19888,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["object-hash", [
["npm:2.1.1", {
"packageLocation": "./.yarn/cache/object-hash-npm-2.1.1-b31a917f31-fe49a0864c.zip/node_modules/object-hash/",
"packageDependencies": [
["object-hash", "npm:2.1.1"]
],
"linkType": "HARD",
}]
]],
["object-inspect", [
["npm:1.9.0", {
"packageLocation": "./.yarn/cache/object-inspect-npm-1.9.0-75d8ab6cd7-63b412167d.zip/node_modules/object-inspect/",
@ -19859,6 +20037,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["oidc-token-hash", [
["npm:5.0.1", {
"packageLocation": "./.yarn/cache/oidc-token-hash-npm-5.0.1-9b98415c82-9090d9a850.zip/node_modules/oidc-token-hash/",
"packageDependencies": [
["oidc-token-hash", "npm:5.0.1"]
],
"linkType": "HARD",
}]
]],
["omit.js", [
["npm:2.0.2", {
"packageLocation": "./.yarn/cache/omit.js-npm-2.0.2-59def9755d-4827dab2a5.zip/node_modules/omit.js/",
@ -19943,6 +20130,22 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["openid-client", [
["npm:4.4.1", {
"packageLocation": "./.yarn/cache/openid-client-npm-4.4.1-4345281c2f-391c866de4.zip/node_modules/openid-client/",
"packageDependencies": [
["openid-client", "npm:4.4.1"],
["got", "npm:11.8.2"],
["jose", "npm:2.0.4"],
["lru-cache", "npm:6.0.0"],
["make-error", "npm:1.3.6"],
["object-hash", "npm:2.1.1"],
["oidc-token-hash", "npm:5.0.1"],
["p-any", "npm:3.0.0"]
],
"linkType": "HARD",
}]
]],
["opn", [
["npm:5.5.0", {
"packageLocation": "./.yarn/cache/opn-npm-5.5.0-9a97e03147-0ea3b6550f.zip/node_modules/opn/",
@ -20059,6 +20262,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["p-any", [
["npm:3.0.0", {
"packageLocation": "./.yarn/cache/p-any-npm-3.0.0-03cf59e600-684f65435b.zip/node_modules/p-any/",
"packageDependencies": [
["p-any", "npm:3.0.0"],
["p-cancelable", "npm:2.0.0"],
["p-some", "npm:5.0.0"]
],
"linkType": "HARD",
}]
]],
["p-cancelable", [
["npm:1.1.0", {
"packageLocation": "./.yarn/cache/p-cancelable-npm-1.1.0-d147d5996f-01fdd9ac31.zip/node_modules/p-cancelable/",
@ -20202,6 +20416,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["p-some", [
["npm:5.0.0", {
"packageLocation": "./.yarn/cache/p-some-npm-5.0.0-80107b767a-cb778e32d6.zip/node_modules/p-some/",
"packageDependencies": [
["p-some", "npm:5.0.0"],
["aggregate-error", "npm:3.1.0"],
["p-cancelable", "npm:2.0.0"]
],
"linkType": "HARD",
}]
]],
["p-try", [
["npm:1.0.0", {
"packageLocation": "./.yarn/cache/p-try-npm-1.0.0-7373139e40-85739d77b3.zip/node_modules/p-try/",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -28,6 +28,7 @@ async function getGraphQl({ context }) {
});
const config = {
CONFIGURATION_BASE_PATH: context.outputDirectory,
development: true,
logger: console,
getSecrets: createGetSecretsFromEnv(),
};

View File

@ -53,9 +53,11 @@
"google-spreadsheet": "3.1.15",
"graphql": "15.5.0",
"graphql-type-json": "0.3.2",
"jsonwebtoken": "8.5.1",
"mingo": "4.1.2",
"moment": "2.29.1",
"mongodb": "3.6.4",
"openid-client": "4.4.1",
"saslprep": "1.0.3"
},
"devDependencies": {

View File

@ -16,20 +16,33 @@
limitations under the License.
*/
import createGetController from './getController';
import { get } from '@lowdefy/helpers';
import createGetController from '../controllers/getController';
import createGetLoader from './getLoader';
function createContext(config) {
const { CONFIGURATION_BASE_PATH, logger, getSecrets } = config;
const { CONFIGURATION_BASE_PATH, logger, getSecrets, development } = config;
const bootstrapContext = {
CONFIGURATION_BASE_PATH,
development,
logger,
getSecrets,
};
async function context() {
// lambda context function signature is ({ event }),
// but express is ({ req })
async function context({ event, req }) {
let headers;
if (event) {
headers = event.headers;
}
if (req) {
headers = req.headers;
}
bootstrapContext.host = get(headers, 'Host') || get(headers, 'host');
bootstrapContext.getLoader = createGetLoader(bootstrapContext);
bootstrapContext.getController = createGetController(bootstrapContext);
return {
getController: createGetController(bootstrapContext),
getController: bootstrapContext.getController,
logger,
};
}

View File

@ -16,15 +16,19 @@
import { type } from '@lowdefy/helpers';
import createPageController from '../controllers/pageController';
import createComponentController from '../controllers/componentController';
import createRequestController from '../controllers/requestController';
import createPageController from './pageController';
import createComponentController from './componentController';
import createOpenIdController from './openIdController';
import createRequestController from './requestController';
import createTokenController from './tokenController';
function creatGetController(context) {
const constructors = {
page: createPageController,
component: createComponentController,
openId: createOpenIdController,
request: createRequestController,
token: createTokenController,
};
const memoized = {};

View File

@ -16,9 +16,11 @@
import createGetController from './getController';
import { testBootstrapContext } from '../test/testContext';
import { PageController } from '../controllers/pageController';
import { ComponentController } from '../controllers/componentController';
import { RequestController } from '../controllers/requestController';
import { PageController } from './pageController';
import { ComponentController } from './componentController';
import { OpenIdController } from './openIdController';
import { RequestController } from './requestController';
import { TokenController } from './tokenController';
test('get page controller', () => {
const getController = createGetController(testBootstrapContext());
@ -32,12 +34,24 @@ test('get component controller', () => {
expect(controller).toBeInstanceOf(ComponentController);
});
test('get openId controller', () => {
const getController = createGetController(testBootstrapContext());
const controller = getController('openId');
expect(controller).toBeInstanceOf(OpenIdController);
});
test('get request controller', () => {
const getController = createGetController(testBootstrapContext());
const controller = getController('request');
expect(controller).toBeInstanceOf(RequestController);
});
test('get token controller', () => {
const getController = createGetController(testBootstrapContext());
const controller = getController('token');
expect(controller).toBeInstanceOf(TokenController);
});
test('memoise controller', () => {
const getController = createGetController(testBootstrapContext());
const controller1 = getController('page');

View File

@ -0,0 +1,160 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { get } from '@lowdefy/helpers';
import { Issuer } from 'openid-client';
import { AuthenticationError, ConfigurationError } from '../context/errors';
class OpenIdController {
constructor({ development, getController, getLoader, getSecrets, host }) {
const httpPrefix = development ? 'http' : 'https';
this.componentLoader = getLoader('component');
this.getSecrets = getSecrets;
this.host = host;
this.redirectUri = `${httpPrefix}://${host}/auth/openid-callback`;
this.tokenController = getController('token');
}
async getOpenIdConfig() {
const { OPENID_CLIENT_ID, OPENID_CLIENT_SECRET, OPENID_DOMAIN } = await this.getSecrets();
if (!(OPENID_CLIENT_ID || OPENID_CLIENT_SECRET || OPENID_DOMAIN)) return null;
if (!(OPENID_CLIENT_ID && OPENID_CLIENT_SECRET && OPENID_DOMAIN)) {
throw new ConfigurationError('Invalid OpenID Connect configuration.');
}
const appConfig = await this.componentLoader.load('config');
const openIdConfig = get(appConfig, 'auth.openId', { default: {} });
return {
...openIdConfig,
clientId: OPENID_CLIENT_ID,
clientSecret: OPENID_CLIENT_SECRET,
domain: OPENID_DOMAIN,
};
}
async authorizationUrl({ location }) {
try {
const config = await this.getOpenIdConfig();
if (!config) return null;
const state = await this.tokenController.issueOpenIdStateToken({
location,
});
return this.getAuthorizationUrl({ config, state });
} catch (error) {
throw new ConfigurationError(error);
}
}
async getAuthorizationUrl({ config, state }) {
const issuer = await Issuer.discover(config.domain);
const client = new issuer.Client({
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uris: [this.redirectUri],
});
const url = client.authorizationUrl({
redirect_uri: this.redirectUri,
response_type: 'code',
scope: config.scope || 'openid profile email',
state,
});
return url;
}
async callback({ code, state }) {
try {
const config = await this.getOpenIdConfig();
if (!config) return null;
const { claims, idToken } = await this.openIdCallback({ code, config });
const { location } = await this.tokenController.verifyOpenIdStateToken(state);
const accessToken = await this.tokenController.issueAccessToken(claims);
return {
accessToken,
idToken,
location,
};
} catch (error) {
throw new AuthenticationError(error);
}
}
async openIdCallback({ code, config }) {
const issuer = await Issuer.discover(config.domain);
const client = new issuer.Client({
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uris: [this.redirectUri],
});
const tokenSet = await client.callback(
this.redirectUri,
{
code,
},
{
response_type: 'code',
}
);
return {
claims: tokenSet.claims(),
idToken: tokenSet.id_token,
};
}
async logoutUrl({ idToken }) {
try {
const config = await this.getOpenIdConfig();
if (!config) return null;
if (config.logoutFromProvider !== true) {
return null;
}
if (config.logoutUri) {
return config.logoutUri;
}
const issuer = await Issuer.discover(config.domain);
const client = new issuer.Client({
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uris: [this.redirectUri],
});
return client.endSessionUrl({
id_token_hint: idToken,
post_logout_redirect_uri: config.postLogoutRedirectUri,
});
} catch (error) {
throw new AuthenticationError(error);
}
}
}
function createOpenIdController(context) {
return new OpenIdController(context);
}
export { OpenIdController };
export default createOpenIdController;

View File

@ -0,0 +1,114 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import jwt from 'jsonwebtoken';
import { AuthenticationError, TokenExpiredError } from '../context/errors';
class TokenController {
constructor({ getLoader, getSecrets, host }) {
this.componentLoader = getLoader('component');
this.host = host;
this.getSecrets = getSecrets;
}
async issueAccessToken({ sub, groups = [] }) {
const { JWT_SECRET } = await this.getSecrets();
return jwt.sign(
{
sub,
type: 'lowdefy_access',
groups,
},
JWT_SECRET,
{
expiresIn: '12h',
audience: this.host,
issuer: this.host,
}
);
}
async verifyAccessToken(token) {
try {
const { JWT_SECRET } = await this.getSecrets();
const claims = jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'],
audience: this.host,
issuer: this.host,
});
if (claims.type !== 'lowdefy_access') {
throw new AuthenticationError('Invalid token');
}
if (!claims.sub) {
throw new AuthenticationError('Invalid token');
}
return claims;
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new TokenExpiredError('Token expired');
} else {
throw new AuthenticationError('Invalid token');
}
}
}
async issueOpenIdStateToken({ location }) {
const { JWT_SECRET } = await this.getSecrets();
return jwt.sign(
{
type: 'openid_state',
location,
},
JWT_SECRET,
{
expiresIn: '5min',
audience: this.host,
issuer: this.host,
}
);
}
async verifyOpenIdStateToken(token) {
try {
const { JWT_SECRET } = await this.getSecrets();
const claims = jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'],
audience: this.host,
issuer: this.host,
});
if (claims.type !== 'openid_state') {
throw new AuthenticationError('Invalid token');
}
return claims;
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new AuthenticationError('Token expired');
} else {
throw new AuthenticationError('Invalid token');
}
}
}
}
function createTokenController(context) {
return new TokenController(context);
}
export { TokenController };
export default createTokenController;

View File

@ -0,0 +1,21 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
async function openIdAuthorizationUrl(_, { openIdAuthorizationUrlInput }, { getController }) {
return getController('openId').authorizationUrl(openIdAuthorizationUrlInput);
}
export default openIdAuthorizationUrl;

View File

@ -0,0 +1,21 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
async function openIdCallback(_, { openIdCallbackInput }, { getController }) {
return getController('openId').callback(openIdCallbackInput);
}
export default openIdCallback;

View File

@ -0,0 +1,21 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
async function openIdLogoutUrl(_, { openIdLogoutUrlInput }, { getController }) {
return getController('openId').logoutUrl(openIdLogoutUrlInput);
}
export default openIdLogoutUrl;

View File

@ -15,8 +15,12 @@
*/
import GraphQLJSON from 'graphql-type-json';
import lowdefyGlobal from './queries/lowdefyGlobal/lowdefyGlobal';
import menu from './queries/menu/menu';
import openIdAuthorizationUrl from './queries/auth/openIdAuthorizationUrl';
import openIdCallback from './queries/auth/openIdCallback';
import openIdLogoutUrl from './queries/auth/openIdLogoutUrl';
import page from './queries/page/page';
import request from './queries/request/request';
@ -32,6 +36,9 @@ const resolvers = {
Query: {
lowdefyGlobal,
menu,
openIdAuthorizationUrl,
openIdCallback,
openIdLogoutUrl,
page,
request,
},

View File

@ -22,6 +22,9 @@ const typeDefs = gql`
page(pageId: ID!): JSON
lowdefyGlobal: JSON
menu: MenuResponse
openIdAuthorizationUrl(openIdAuthorizationUrlInput: OpenIdAuthorizationUrlInput!): String
openIdCallback(openIdCallbackInput: OpenIdCallbackInput!): OpenIdCallbackResponse
openIdLogoutUrl(openIdLogoutUrlInput: OpenIdLogoutUrlInput!): String
request(input: RequestInput!): RequestResponse
}
@ -66,6 +69,25 @@ const typeDefs = gql`
url: String
}
input OpenIdAuthorizationUrlInput {
location: String
}
input OpenIdCallbackInput {
code: String!
state: String!
}
type OpenIdCallbackResponse {
accessToken: String
idToken: String
location: String
}
input OpenIdLogoutUrlInput {
idToken: String
}
type RequestResponse {
id: ID!
type: String

View File

@ -24,6 +24,7 @@ import { createGetSecretsFromEnv } from '@lowdefy/node-utils';
dotenv.config({ silent: true });
const config = {
CONFIGURATION_BASE_PATH: path.resolve(process.cwd(), './.lowdefy/build'),
development: true,
logger: console,
getSecrets: createGetSecretsFromEnv(),
};

198
yarn.lock
View File

@ -3034,9 +3034,11 @@ __metadata:
graphql: 15.5.0
graphql-type-json: 0.3.2
jest: 26.6.3
jsonwebtoken: 8.5.1
mingo: 4.1.2
moment: 2.29.1
mongodb: 3.6.4
openid-client: 4.4.1
saslprep: 1.0.3
webpack: 5.22.0
webpack-cli: 4.5.0
@ -3491,6 +3493,13 @@ __metadata:
languageName: node
linkType: hard
"@panva/asn1.js@npm:^1.0.0":
version: 1.0.0
resolution: "@panva/asn1.js@npm:1.0.0"
checksum: 0563c1372d99051d8e2268541a6ef9ec5d0495e9b943b0b1b738eb40ffd21e8bf7690f313d5ce86cccb32e646f9bb75f1af3cd45bce5d04b85ad9c04f0b596aa
languageName: node
linkType: hard
"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
version: 1.1.2
resolution: "@protobufjs/aspromise@npm:1.1.2"
@ -4863,6 +4872,16 @@ __metadata:
languageName: node
linkType: hard
"aggregate-error@npm:^3.0.0":
version: 3.1.0
resolution: "aggregate-error@npm:3.1.0"
dependencies:
clean-stack: ^2.0.0
indent-string: ^4.0.0
checksum: 704d2001a303c185e9b836d211f7eef2f4557195a11c3271143b4dcda5f6f263abe746d9b8a06b5871d07870686c7db9c0b2c38e2d3cbc593784eaaee8a29043
languageName: node
linkType: hard
"airbnb-prop-types@npm:^2.16.0":
version: 2.16.0
resolution: "airbnb-prop-types@npm:2.16.0"
@ -6739,6 +6758,13 @@ __metadata:
languageName: node
linkType: hard
"clean-stack@npm:^2.0.0":
version: 2.2.0
resolution: "clean-stack@npm:2.2.0"
checksum: e291ce2b8c8c59e6449ac9a7a726090264bea6696e5343b21385e16d279c808ca09d73a1abea8fd23a9b7699e6ef5ce582df203511f71c8c27666bf3b2e300c5
languageName: node
linkType: hard
"clean-webpack-plugin@npm:3.0.0":
version: 3.0.0
resolution: "clean-webpack-plugin@npm:3.0.0"
@ -10323,6 +10349,25 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"got@npm:^11.8.0":
version: 11.8.2
resolution: "got@npm:11.8.2"
dependencies:
"@sindresorhus/is": ^4.0.0
"@szmarczak/http-timer": ^4.0.5
"@types/cacheable-request": ^6.0.1
"@types/responselike": ^1.0.0
cacheable-lookup: ^5.0.3
cacheable-request: ^7.0.1
decompress-response: ^6.0.0
http2-wrapper: ^1.0.0-beta.5.2
lowercase-keys: ^2.0.0
p-cancelable: ^2.0.0
responselike: ^2.0.0
checksum: 6415f98ec249e932ca8223396e58ec18017ade64e21efc40db62f994f3551eaf7eca945671d40b2486f0650b1f17b5a2a0f11655ea501712566ef60be010f07f
languageName: node
linkType: hard
"got@npm:^9.6.0":
version: 9.6.0
resolution: "got@npm:9.6.0"
@ -12452,6 +12497,15 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"jose@npm:^2.0.4":
version: 2.0.4
resolution: "jose@npm:2.0.4"
dependencies:
"@panva/asn1.js": ^1.0.0
checksum: 402054b230df5856278e154d7f4e2707eda9f45e50e0fd273f4b4f0823ff9316ac72438701b0124b58f5927f479722db35a4b204359fe802f9a974b620fca267
languageName: node
linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
@ -12701,6 +12755,24 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"jsonwebtoken@npm:8.5.1":
version: 8.5.1
resolution: "jsonwebtoken@npm:8.5.1"
dependencies:
jws: ^3.2.2
lodash.includes: ^4.3.0
lodash.isboolean: ^3.0.3
lodash.isinteger: ^4.0.4
lodash.isnumber: ^3.0.3
lodash.isplainobject: ^4.0.6
lodash.isstring: ^4.0.1
lodash.once: ^4.0.0
ms: ^2.1.1
semver: ^5.6.0
checksum: ea44bbb7a7abab87eee57711a04093a2a04a5757faa3253f521327b840e07eff751986ed6c20985886c5aaa86245a833aa1eeda6eacb0c3fa9ea4e7074cdb0c3
languageName: node
linkType: hard
"jsprim@npm:^1.2.2":
version: 1.4.1
resolution: "jsprim@npm:1.4.1"
@ -12723,6 +12795,17 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"jwa@npm:^1.4.1":
version: 1.4.1
resolution: "jwa@npm:1.4.1"
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: ^5.0.1
checksum: e3a6234a3a33b0390c35cc7393890b1ca1bac22382755035ad252f6df9e1272dcf0ff0a717c7865657fc30e04b8d5ab07ea27e26f0763c60c557946963402752
languageName: node
linkType: hard
"jwa@npm:^2.0.0":
version: 2.0.0
resolution: "jwa@npm:2.0.0"
@ -12734,6 +12817,16 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"jws@npm:^3.2.2":
version: 3.2.2
resolution: "jws@npm:3.2.2"
dependencies:
jwa: ^1.4.1
safe-buffer: ^5.0.1
checksum: 3990b26ebb6f368bf6c53bf580cd327f207052adedeb7900dde665e143fff9ea5d96d0b4282e85a631a6e3af76ada281a1ccc450b1916d579d07e9d36b564a19
languageName: node
linkType: hard
"jws@npm:^4.0.0":
version: 4.0.0
resolution: "jws@npm:4.0.0"
@ -13113,6 +13206,20 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"lodash.includes@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.includes@npm:4.3.0"
checksum: 20d6b1bf7841a4eb21bcb124641a1d5fb368e3c86fb8834d80149ec92caef67f3bf316405e4ec309591f0a83e461cebb7b43b2b57d00fd45d1eb3009fe13be97
languageName: node
linkType: hard
"lodash.isboolean@npm:^3.0.3":
version: 3.0.3
resolution: "lodash.isboolean@npm:3.0.3"
checksum: e5b7a921f47759773266e66664e4a54f2438cc213fe336c5b96321d4328ebb2785e939bbcd07b29d5a20cdc6140d471abae1e94cdfb25937dc91ddc5150f41a0
languageName: node
linkType: hard
"lodash.isequal@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.isequal@npm:4.5.0"
@ -13120,6 +13227,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"lodash.isinteger@npm:^4.0.4":
version: 4.0.4
resolution: "lodash.isinteger@npm:4.0.4"
checksum: a29551cc9aaaf24cc4fdd7ce1734c7a19c748c704f061ef079f6026dc9666ac10e8ae08e4f0cd3aeb6094e20be043a7402c72ce70cbae9ea6ecce67ad997332c
languageName: node
linkType: hard
"lodash.ismatch@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.ismatch@npm:4.4.0"
@ -13127,6 +13241,27 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"lodash.isnumber@npm:^3.0.3":
version: 3.0.3
resolution: "lodash.isnumber@npm:3.0.3"
checksum: a33b10bf57dd27b32aac0bb4159d3f40db6b294095f42d7ff9f966a004932cdcc12417bf0750a9d3ac0c62690dd020d7d227b05ef486a8c9061963acf2ea3fad
languageName: node
linkType: hard
"lodash.isplainobject@npm:^4.0.6":
version: 4.0.6
resolution: "lodash.isplainobject@npm:4.0.6"
checksum: 72a114b610ec32a42b8cb47680d1729398caea0ee0631c0b220b97b21e7df19312377cb077acb6593bf6c5abdbdb43c530aa66b440e30d53324986d386808cd0
languageName: node
linkType: hard
"lodash.isstring@npm:^4.0.1":
version: 4.0.1
resolution: "lodash.isstring@npm:4.0.1"
checksum: 20c46960b74fd63c27b534f1725cd4141ac19b35c7250affee37c8b7899b1a4c5e9820becfafe571a4d48cd4c86206ee03c2e93fe943dbeab82ddd5cab710540
languageName: node
linkType: hard
"lodash.merge@npm:4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
@ -13134,6 +13269,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"lodash.once@npm:^4.0.0":
version: 4.1.1
resolution: "lodash.once@npm:4.1.1"
checksum: 236e00ca5f20304fab5b38aa3aedb034959153dae6edf33d7f9b00406ced8f24ed232a74f1200505d9049165ceea2ce1256199e1683b0a25e9de89091d4b13c2
languageName: node
linkType: hard
"lodash.set@npm:^4.3.2":
version: 4.3.2
resolution: "lodash.set@npm:4.3.2"
@ -13382,6 +13524,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"make-error@npm:^1.3.6":
version: 1.3.6
resolution: "make-error@npm:1.3.6"
checksum: 2c780bab8409b865e8ee86697c599a2bf2765ec64d21eb67ccda27050e039f983feacad05a0d43aba3c966ea03d305d2612e94fec45474bcbc61181f57c5bb88
languageName: node
linkType: hard
"make-fetch-happen@npm:^5.0.0":
version: 5.0.2
resolution: "make-fetch-happen@npm:5.0.2"
@ -14770,6 +14919,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"object-hash@npm:^2.0.1":
version: 2.1.1
resolution: "object-hash@npm:2.1.1"
checksum: fe49a0864cba7ac4055c604295692ae75f5f4d22ba929aaaa987469809d17ab030d1111e8f0d425a070fa4a78b8021b55260e26e98b66b59e0f7be2bd9069fb8
languageName: node
linkType: hard
"object-inspect@npm:^1.7.0, object-inspect@npm:^1.8.0, object-inspect@npm:^1.9.0":
version: 1.9.0
resolution: "object-inspect@npm:1.9.0"
@ -14892,6 +15048,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"oidc-token-hash@npm:^5.0.1":
version: 5.0.1
resolution: "oidc-token-hash@npm:5.0.1"
checksum: 9090d9a850ec07fd322915a8fdcbe48b8050843afbe38643c5dea52627cc6b8600a234a1a8b17514f3aa775982ab2934f242298c59a558588b7a677022207c6b
languageName: node
linkType: hard
"omit.js@npm:^2.0.0, omit.js@npm:^2.0.2":
version: 2.0.2
resolution: "omit.js@npm:2.0.2"
@ -14970,6 +15133,21 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"openid-client@npm:4.4.1":
version: 4.4.1
resolution: "openid-client@npm:4.4.1"
dependencies:
got: ^11.8.0
jose: ^2.0.4
lru-cache: ^6.0.0
make-error: ^1.3.6
object-hash: ^2.0.1
oidc-token-hash: ^5.0.1
p-any: ^3.0.0
checksum: 391c866de47f48e467b77e540e09b883b877ecc4fc9a4f7da858b1568d20f7872a4431811b3932bc2323fc28fb0f327d732f8cb207c055e7a5aaef5b3af53a0b
languageName: node
linkType: hard
"opn@npm:^5.5.0":
version: 5.5.0
resolution: "opn@npm:5.5.0"
@ -15076,6 +15254,16 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"p-any@npm:^3.0.0":
version: 3.0.0
resolution: "p-any@npm:3.0.0"
dependencies:
p-cancelable: ^2.0.0
p-some: ^5.0.0
checksum: 684f65435bb3d1c43c671d860c42b67d5a5cdc07a929a62ba315d1dadc2bd20cdab15d8ab10d6369fc96065c9d71bad9393a6b1df7a9d97c8d9f6d87a34af770
languageName: node
linkType: hard
"p-cancelable@npm:^1.0.0":
version: 1.1.0
resolution: "p-cancelable@npm:1.1.0"
@ -15206,6 +15394,16 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"p-some@npm:^5.0.0":
version: 5.0.0
resolution: "p-some@npm:5.0.0"
dependencies:
aggregate-error: ^3.0.0
p-cancelable: ^2.0.0
checksum: cb778e32d64555db610cb1bbbec145b10bd541d1219996349aa515998aa89ea87a85c6c9c4629763a11fc1769c2b3e1656d12ab9cfcc77eaf8fc1acc080661de
languageName: node
linkType: hard
"p-try@npm:^1.0.0":
version: 1.0.0
resolution: "p-try@npm:1.0.0"