From ea7b2c456b4a0734cd05e365d39735a5ae438949 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 7 Feb 2020 16:20:16 -0600 Subject: [PATCH] AIOSEC-1 Protect auth-server with admin token (legacy style) --- services/auth-server/package.json | 1 + services/auth-server/src/index.js | 11 ++++- services/auth-server/src/util/auth.js | 68 +++++++++++++++++++++++++++ services/ssh/home/grant.sh | 5 +- services/ssh/home/token-debug.sh | 16 +++---- services/ssh/home/token.sh | 5 +- yarn.lock | 2 +- 7 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 services/auth-server/src/util/auth.js diff --git a/services/auth-server/package.json b/services/auth-server/package.json index 48139b51de..6d85019c37 100644 --- a/services/auth-server/package.json +++ b/services/auth-server/package.json @@ -23,6 +23,7 @@ "body-parser": "^1.18.2", "express": "^4.16.2", "flow-remove-types": "^1.2.3", + "jsonwebtoken": "^8.5.1", "morgan": "^1.9.0", "nano": "^6.4.3", "ramda": "^0.25.0", diff --git a/services/auth-server/src/index.js b/services/auth-server/src/index.js index 09310ef78a..b5eff5571e 100644 --- a/services/auth-server/src/index.js +++ b/services/auth-server/src/index.js @@ -6,6 +6,7 @@ const express = require('express'); const morgan = require('morgan'); const axios = require('axios'); const logger = require('./logger'); +const validateToken = require('./util/auth'); const { generateRoute } = require('./routes'); import type { LagoonErrorWithStatus, $Request, $Response } from 'express'; @@ -25,6 +26,9 @@ app.use( }), ); +// Only allow access with valid, admin Bearer (JWT) token +app.use(validateToken); + const port = process.env.PORT || 3000; const lagoonKeycloakRoute = R.compose( R.defaultTo('http://keycloak:8080'), @@ -62,8 +66,13 @@ const getUserGrant = async (userId: string): Promise => { app.post('/generate', ...generateRoute(getUserGrant)); -app.use((err: LagoonErrorWithStatus, req: $Request, res: $Response) => { +app.use((err: LagoonErrorWithStatus, req: $Request, res: $Response, next: Function) => { logger.error(err.toString()); + + if (res.headersSent) { + return next(err) + } + res.status(err.status || 500); res.send(`Request failed: ${err.toString()}`); }); diff --git a/services/auth-server/src/util/auth.js b/services/auth-server/src/util/auth.js new file mode 100644 index 0000000000..f8b614341f --- /dev/null +++ b/services/auth-server/src/util/auth.js @@ -0,0 +1,68 @@ +const R = require('ramda'); +const logger = require('../logger'); +const JWT = require('jsonwebtoken'); + +const { JWTSECRET, JWTAUDIENCE } = process.env; + +const parseBearerToken = R.compose( + R.ifElse( + splits => + R.length(splits) === 2 && + R.compose( + R.toLower, + R.defaultTo(''), + R.head, + )(splits) === 'bearer', + R.nth(1), + R.always(null), + ), + R.split(' '), + R.defaultTo(''), +); + +const validateToken = async ( + req, + res, + next, +) => { + const token = parseBearerToken(req.get('Authorization')); + + if (token == null) { + logger.debug('No Bearer Token'); + return res + .status(401) + .send({ errors: [{ message: 'Unauthorized - Bearer Token Required' }] }); + } + + try { + decoded = JWT.verify(token, JWTSECRET); + + if (decoded == null) { + throw new Error('Decoding token resulted in "null" or "undefined".'); + } + + const { aud } = decoded; + + if (JWTAUDIENCE && aud !== JWTAUDIENCE) { + logger.info(`Invalid token with aud attribute: "${aud || ''}"`); + throw new Error('Token audience mismatch.'); + } + + const { role = 'none' } = decoded; + + if (role !== 'admin') { + throw new Error('Cannot authenticate non-admin user with legacy token.'); + } + + next(); + return; + } catch (e) { + return res.status(403).send({ + errors: [{ message: `Forbidden - Invalid Auth Token: ${e.message}` }], + }); + } + + next(); +}; + +module.exports = validateToken; diff --git a/services/ssh/home/grant.sh b/services/ssh/home/grant.sh index a2840928c5..4b5453d197 100755 --- a/services/ssh/home/grant.sh +++ b/services/ssh/home/grant.sh @@ -24,11 +24,12 @@ ADMIN_GRAPHQL="query GetUserIdBySshKey { ADMIN_QUERY=$(echo $ADMIN_GRAPHQL | sed 's/"/\\"/g' | sed 's/\\n/\\\\n/g' | awk -F'\n' '{if(NR == 1) {printf $0} else {printf "\\n"$0}}') USER_ID=$(curl -s -XPOST -H 'Content-Type: application/json' -H "$ADMIN_BEARER" api:3000/graphql -d "{\"query\": \"$ADMIN_QUERY\"}" | jq --raw-output '.data.userBySshKey.id') -header="Content-Type: application/json" +CONTENT_TYPE="Content-Type: application/json" +AUTHORIZATION="Authorization: Bearer $API_ADMIN_TOKEN" # Prepare the post (containing the user id) as a JSON object. data="{\"userId\": \"$USER_ID\", \"grant\": true}" # Submit the token request as a POST request with the JSON data # containing the key. -echo $(wget "$server/generate" --header "$header" --post-data "$data" -q -O -) +echo $(wget "$server/generate" --header "$CONTENT_TYPE" --header "$AUTHORIZATION" --post-data "$data" -q -O -) diff --git a/services/ssh/home/token-debug.sh b/services/ssh/home/token-debug.sh index 8f809d5876..e2280794ac 100755 --- a/services/ssh/home/token-debug.sh +++ b/services/ssh/home/token-debug.sh @@ -5,12 +5,9 @@ USER_SSH_KEY=$2 echo "USER_SSH_KEY='${USER_SSH_KEY}'" >> /proc/1/fd/1 -# This variable is replaced by envplate inside docker-entrypoint. -# We need this because during execution time inside the SSH -# connection we don't have access to the container environment -# variables. -# So we replace it during the start of the container. -server=${AUTH_SERVER} +# This variable is hardcoded because using envplate would wipe out all the +# other variables we want to debug. +server="http://auth-server:3000" echo "server='${server}'" >> /proc/1/fd/1 @@ -29,14 +26,15 @@ USER_ID=$(curl -s -XPOST -H 'Content-Type: application/json' -H "$ADMIN_BEARER" echo "USER_ID='${USER_ID}'" >> /proc/1/fd/1 -header="Content-Type: application/json" +CONTENT_TYPE="Content-Type: application/json" +AUTHORIZATION="Authorization: Bearer $API_ADMIN_TOKEN" # Prepare the post (containing the user id) as a JSON object. data="{\"userId\": \"$USER_ID\"}" -token=$(wget "$server/generate" --header "$header" --post-data "$data" -d -v -O - 2>&1) +token=$(wget "$server/generate" --header "$CONTENT_TYPE" --header "$AUTHORIZATION" --post-data "$data" -d -v -O - 2>&1) echo "token='${token}'" >> /proc/1/fd/1 # Submit the token request as a POST request with the JSON data # containing the key. -echo $(wget "$server/generate" --header "$header" --post-data "$data" -q -O -) +echo $(wget "$server/generate" --header "$CONTENT_TYPE" --header "$AUTHORIZATION" --post-data "$data" -q -O -) diff --git a/services/ssh/home/token.sh b/services/ssh/home/token.sh index fa43aff0d7..cf81996449 100755 --- a/services/ssh/home/token.sh +++ b/services/ssh/home/token.sh @@ -24,11 +24,12 @@ ADMIN_GRAPHQL="query GetUserIdBySshKey { ADMIN_QUERY=$(echo $ADMIN_GRAPHQL | sed 's/"/\\"/g' | sed 's/\\n/\\\\n/g' | awk -F'\n' '{if(NR == 1) {printf $0} else {printf "\\n"$0}}') USER_ID=$(curl -s -XPOST -H 'Content-Type: application/json' -H "$ADMIN_BEARER" api:3000/graphql -d "{\"query\": \"$ADMIN_QUERY\"}" | jq --raw-output '.data.userBySshKey.id') -header="Content-Type: application/json" +CONTENT_TYPE="Content-Type: application/json" +AUTHORIZATION="Authorization: Bearer $API_ADMIN_TOKEN" # Prepare the post (containing the user id) as a JSON object. data="{\"userId\": \"$USER_ID\"}" # Submit the token request as a POST request with the JSON data # containing the key. -echo $(wget "$server/generate" --header "$header" --post-data "$data" -q -O -) +echo $(wget "$server/generate" --header "$CONTENT_TYPE" --header "$AUTHORIZATION" --post-data "$data" -q -O -) diff --git a/yarn.lock b/yarn.lock index b5ea007b27..a832e11eaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10179,7 +10179,7 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonwebtoken@^8.0.1: +jsonwebtoken@^8.0.1, jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==