feat(graphql): Set and unset authorization cookie.

This commit is contained in:
SamTolmay 2021-03-05 18:06:13 +02:00
parent f7573fee3c
commit 8abe43cf99
11 changed files with 100 additions and 10 deletions

8
.pnp.cjs generated
View File

@ -4448,6 +4448,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["babel-jest", "virtual:caddf51df4928b33a437ca87b8f5ddfb6205ebd6d8231f74d4ee7223f3866e6f815b221aa1e2bd33e98915f701e95bae72a93d2288b49a34a6246bdbc2a4a132#npm:26.6.3"],
["babel-loader", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:8.2.2"],
["clean-webpack-plugin", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:3.0.0"],
["cookie", "npm:0.4.1"],
["dataloader", "npm:2.0.0"],
["google-spreadsheet", "npm:3.1.15"],
["graphql", "npm:15.5.0"],
@ -10661,6 +10662,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["cookie", "npm:0.4.0"]
],
"linkType": "HARD",
}],
["npm:0.4.1", {
"packageLocation": "./.yarn/cache/cookie-npm-0.4.1-cc5e2ebb42-b8e0928e3e.zip/node_modules/cookie/",
"packageDependencies": [
["cookie", "npm:0.4.1"]
],
"linkType": "HARD",
}]
]],
["cookie-signature", [

Binary file not shown.

View File

@ -49,6 +49,7 @@
"apollo-server": "2.21.0",
"aws-sdk": "2.845.0",
"axios": "0.21.1",
"cookie": "0.4.1",
"dataloader": "2.0.0",
"google-spreadsheet": "3.1.15",
"graphql": "15.5.0",

View File

@ -19,9 +19,17 @@
import { get } from '@lowdefy/helpers';
import createGetController from '../controllers/getController';
import createGetLoader from './getLoader';
import verifyAccessToken from './verifyAccessToken';
function createContext(config) {
const { CONFIGURATION_BASE_PATH, development, getHeaders, getSecrets, logger } = config;
const {
CONFIGURATION_BASE_PATH,
development,
getHeaders,
getSecrets,
getSetHeader,
logger,
} = config;
const bootstrapContext = {
CONFIGURATION_BASE_PATH,
development,
@ -29,10 +37,14 @@ function createContext(config) {
logger,
};
async function context(input) {
const headers = getHeaders(input);
bootstrapContext.host = get(headers, 'Host') || get(headers, 'host');
bootstrapContext.headers = getHeaders(input);
bootstrapContext.setHeader = getSetHeader(input);
bootstrapContext.host =
get(bootstrapContext.headers, 'Host') || get(bootstrapContext.headers, 'host');
bootstrapContext.getLoader = createGetLoader(bootstrapContext);
bootstrapContext.getController = createGetController(bootstrapContext);
bootstrapContext.user = await verifyAccessToken(bootstrapContext);
console.log(bootstrapContext.user);
return {
getController: bootstrapContext.getController,
logger,

View File

@ -0,0 +1,41 @@
/*
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.
*/
/* eslint-disable no-unused-vars */
import { get } from '@lowdefy/helpers';
import cookie from 'cookie';
async function verifyAccessToken({ headers, getController }) {
const cookieHeader = get(headers, 'Cookie') || get(headers, 'cookie') || '';
const { authorization } = cookie.parse(cookieHeader);
if (authorization) {
const tokenController = getController('token');
const {
iat,
exp,
aud,
iss,
lowdefy_access_token,
...user
} = await tokenController.verifyAccessToken(authorization);
return user;
}
return {};
}
export default verifyAccessToken;

View File

@ -16,17 +16,20 @@
import { get } from '@lowdefy/helpers';
import { Issuer } from 'openid-client';
import cookie from 'cookie';
import { AuthenticationError, ConfigurationError } from '../context/errors';
class OpenIdController {
constructor({ development, getController, getLoader, getSecrets, host }) {
constructor({ development, getController, getLoader, getSecrets, host, setHeader }) {
const httpPrefix = development ? 'http' : 'https';
this.development = development;
this.componentLoader = getLoader('component');
this.getSecrets = getSecrets;
this.host = host;
this.redirectUri = `${httpPrefix}://${host}/auth/openid-callback`;
this.setHeader = setHeader;
this.tokenController = getController('token');
}
@ -97,8 +100,14 @@ class OpenIdController {
const { claims, idToken } = await this.openIdCallback({ code, config });
const accessToken = await this.tokenController.issueAccessToken(claims);
const setCookieHeader = cookie.serialize('authorization', accessToken, {
httpOnly: true,
path: '/api/graphql',
sameSite: 'lax',
secure: !this.development,
});
await this.setHeader('Set-Cookie', setCookieHeader);
return {
accessToken,
idToken,
input,
pageId,
@ -128,6 +137,14 @@ class OpenIdController {
async logoutUrl({ idToken }) {
try {
const setCookieHeader = cookie.serialize('authorization', '', {
httpOnly: true,
path: '/api/graphql',
sameSite: 'lax',
secure: !this.development,
maxAge: 0,
});
await this.setHeader('Set-Cookie', setCookieHeader);
const config = await this.getOpenIdConfig();
if (!config) return null;

View File

@ -81,7 +81,6 @@ const typeDefs = gql`
}
type OpenIdCallbackResponse {
accessToken: String
idToken: String
input: JSON
pageId: String

View File

@ -22,7 +22,6 @@ import setupLink from '../setupLink';
const OPENID_CALLBACK = gql`
query openIdCallback($openIdCallbackInput: OpenIdCallbackInput!) {
openIdCallback(openIdCallbackInput: $openIdCallbackInput) {
accessToken
idToken
input
pageId
@ -93,7 +92,10 @@ async function openIdCallbackFn({ rootContext, routeHistory, search }) {
const idToken = get(data, 'openIdCallback.idToken');
if (!idToken) throw new Error('Authentication error.');
rootContext.window.localStorage.setItem('idToken', idToken);
rootContext.user = parseJwt(idToken);
// eslint-disable-next-line no-unused-vars
const { iat, exp, aud, iss, ...user } = parseJwt(idToken);
rootContext.user = user;
const { data: menuData } = await rootContext.client.query({
query: GET_MENU,

View File

@ -29,7 +29,7 @@ const cache = new InMemoryCache({
});
const retryLink = new RetryLink();
const httpLink = ({ uri = 'api/graphql' }) => new HttpLink({ uri });
const httpLink = ({ uri = 'api/graphql' }) => new HttpLink({ uri, credentials: 'same-origin' });
// TODO: Handle errors
const errorHandler = ({ graphQLErrors, networkError }) => {

View File

@ -26,7 +26,9 @@ const config = {
CONFIGURATION_BASE_PATH: path.resolve(process.cwd(), './.lowdefy/build'),
development: true,
getHeaders: ({ req }) => req.headers,
setCookie: ({ res }) => {},
getSetHeader: ({ res }) => (name, value) => {
res.set(name, value);
},
getSecrets: createGetSecretsFromEnv(),
logger: console,
};

View File

@ -3029,6 +3029,7 @@ __metadata:
babel-jest: 26.6.3
babel-loader: 8.2.2
clean-webpack-plugin: 3.0.0
cookie: 0.4.1
dataloader: 2.0.0
google-spreadsheet: 3.1.15
graphql: 15.5.0
@ -7359,6 +7360,13 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:0.4.1":
version: 0.4.1
resolution: "cookie@npm:0.4.1"
checksum: b8e0928e3e7aba013087974b33a6eec730b0a68b7ec00fc3c089a56ba2883bcf671252fc2ed64775aa1ca64796b6e1f6fdddba25a66808aef77614d235fd3e06
languageName: node
linkType: hard
"copy-concurrently@npm:^1.0.0":
version: 1.0.5
resolution: "copy-concurrently@npm:1.0.5"