diff --git a/.circleci/config.yml b/.circleci/config.yml index fa4c5fd..6dc3004 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,8 +66,7 @@ workflows: context : org-global filters: branches: - only: - - develop + only: [develop, "feature/Auth0-RS256-Token"] # Production builds are exectuted only on tagged commits to the # master branch. @@ -75,4 +74,4 @@ workflows: context : org-global filters: branches: - only: master \ No newline at end of file + only: master diff --git a/docker/Dockerfile b/docker/Dockerfile index 070074c..34aec64 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Use the base image with Node.js -FROM node:8.12 +FROM node:14 # Copy the current directory into the Docker image COPY . /topcoder-x-ui @@ -12,4 +12,4 @@ RUN npm install RUN npm run build #RUN npm test -CMD npm start \ No newline at end of file +CMD npm start diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 0000000..00b93ec --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "graceful-fs": { + "version": "4.2.2" + } + } +} diff --git a/package.json b/package.json index a680622..9d6d615 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,13 @@ "private": true, "main": "src/app.js", "engines": { - "node": "8", - "npm": "5" + "node": "14", + "npm": "6" }, "scripts": { - "start": "node src/app.js", + "start": "node -r esm src/app.js", "serve": "./node_modules/.bin/concurrently \"npm run start:be\" \"npm run start:fe\"", - "start:be": "nodemon src/app.js ", + "start:be": "nodemon -r esm src/app.js", "start:fe": "gulp build:watch", "build": "gulp build", "test": "node ./node_modules/mocha/bin/mocha --recursive --timeout 999999 --colors tests/*.test.js --bail", @@ -38,6 +38,7 @@ "angular-ui-bootstrap": "~2.5.0", "angular-ui-router": "~1.0.23", "angularjs-datepicker": "^2.1.23", + "atob": "^2.1.2", "auth0-angular": "~4.0.4", "auth0-js": "^9.11.3", "auth0-lock": "^11.17.2", @@ -52,6 +53,7 @@ "cors": "^2.8.4", "debug": "~2.6.3", "dynamoose": "^1.1.0", + "esm": "^3.2.25", "express": "^4.15.4", "express-jwt": "^5.3.0", "express-session": "^1.15.5", @@ -81,7 +83,8 @@ "superagent-promise": "^1.1.0", "typescript": "~2.3.3", "uuid": "^3.3.2", - "winston": "^2.3.1" + "winston": "^2.3.1", + "@topcoder-platform/tc-auth-lib": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.0" }, "devDependencies": { "angular-mocks": "~1.4.4", diff --git a/src/app.js b/src/app.js index cbb31f8..264f55f 100644 --- a/src/app.js +++ b/src/app.js @@ -15,7 +15,7 @@ const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const cookieParser = require('cookie-parser'); -const jwtDecode = require('jwt-decode'); +const decodeToken = require('@topcoder-platform/tc-auth-lib').decodeToken; // const secure = require('ssl-express-www'); const config = require('./config'); const routes = require('./routes'); @@ -23,6 +23,7 @@ const logger = require('./common/logger'); const errors = require('./common/errors'); const constants = require('./common/constants'); const {getAppHealth} = require('./controllers/AppHealthController'); +global.atob = require('atob'); const app = express(); app.use(cors()); @@ -45,7 +46,7 @@ _.forEach(routes, (verbs, path) => { actions.push((req, res, next) => { const v3jwt = _.get(req.cookies, constants.JWT_V3_NAME); if (v3jwt) { - const decoded = jwtDecode(v3jwt); + const decoded = decodeToken(v3jwt); req.currentUser = { handle: decoded.handle.toLowerCase(), roles: decoded.roles, diff --git a/src/config.js b/src/config.js index 0ba1df4..244c1b6 100644 --- a/src/config.js +++ b/src/config.js @@ -61,11 +61,11 @@ module.exports = { }, TOPCODER_VALUES: { dev: { - TC_LOGIN_URL: process.env.TC_LOGIN_URL || 'https://accounts.topcoder-dev.com/member', + TC_LOGIN_URL: process.env.TC_LOGIN_URL || 'https://accounts-auth0.topcoder-dev.com', TC_USER_PROFILE_URL: process.env.TC_USER_PROFILE_URL || 'https://api.topcoder-dev.com/v2/user/profile', }, prod: { - TC_LOGIN_URL: process.env.TC_LOGIN_URL || 'https://accounts.topcoder.com/member', + TC_LOGIN_URL: process.env.TC_LOGIN_URL || 'https://accounts-auth0.topcoder.com', TC_USER_PROFILE_URL: process.env.TC_USER_PROFILE_URL || 'https://api.topcoder.com/v2/user/profile', }, }, @@ -76,11 +76,11 @@ const frontendConfigs = { "JWT_V3_NAME":"v3jwt", "JWT_V2_NAME":"tcjwt", "COOKIES_SECURE":false, - "TC_LOGIN_URL": "https://accounts.topcoder-dev.com/member", + "TC_LOGIN_URL": "https://accounts-auth0.topcoder-dev.com", "TC_USER_PROFILE_URL": "http://api.topcoder-dev.com/v2/user/profile", "API_URL": "https://127.0.0.1:8443", "ADMIN_TOOL_URL": "http://localhost:8080/api/v2", - "ACCOUNTS_CONNECTOR_URL": "https://accounts.topcoder-dev.com/connector.html", + "ACCOUNTS_CONNECTOR_URL": "https://accounts-auth0.topcoder-dev.com", "DIRECT_URL_BASE": "https://www.topcoder-dev/direct/projectOverview?formData.projectId=", "OWNER_LOGIN_GITHUB_URL":"/api/v1/github/owneruser/login", "OWNER_LOGIN_GITLAB_URL":"/api/v1/gitlab/owneruser/login", @@ -92,11 +92,11 @@ const frontendConfigs = { "JWT_V3_NAME":"v3jwt", "JWT_V2_NAME":"tcjwt", "COOKIES_SECURE":false, - "TC_LOGIN_URL": "https://accounts.topcoder-dev.com/member", + "TC_LOGIN_URL": "https://accounts-auth0.topcoder-dev.com", "TC_USER_PROFILE_URL": "https://api.topcoder-dev.com/v2/user/profile", "API_URL": "https://api.topcoder-dev.com", "ADMIN_TOOL_URL": "https://api.topcoder-dev.com/v2", - "ACCOUNTS_CONNECTOR_URL": "https://accounts.topcoder-dev.com/connector.html", + "ACCOUNTS_CONNECTOR_URL": "https://accounts-auth0.topcoder-dev.com", "DIRECT_URL_BASE": "https://www.topcoder-dev.com/direct/projectOverview?formData.projectId=", "OWNER_LOGIN_GITHUB_URL":"/api/v1/github/owneruser/login", "OWNER_LOGIN_GITLAB_URL":"/api/v1/gitlab/owneruser/login", @@ -108,11 +108,14 @@ const frontendConfigs = { "JWT_V3_NAME":"v3jwt", "JWT_V2_NAME":"tcjwt", "COOKIES_SECURE":false, - "TC_LOGIN_URL": "https://accounts.topcoder-dev.com/member", + "TC_LOGIN_URL": "https://accounts-auth0.topcoder-dev.com", + + // TODO: we can clean this conf, as no need https://github.com/topcoder-platform/topcoder-x-ui/issues/342 "TC_USER_PROFILE_URL": "https://api.topcoder-dev.com/v2/user/profile", + "API_URL": "https://api.topcoder-dev.com", "ADMIN_TOOL_URL": "https://api.topcoder-dev.com/v2", - "ACCOUNTS_CONNECTOR_URL": "https://accounts.topcoder-dev.com/connector.html", + "ACCOUNTS_CONNECTOR_URL": "https://accounts-auth0.topcoder-dev.com", "DIRECT_URL_BASE": "https://www.topcoder-dev.com/direct/projectOverview?formData.projectId=", "OWNER_LOGIN_GITHUB_URL":"/api/v1/github/owneruser/login", "OWNER_LOGIN_GITLAB_URL":"/api/v1/gitlab/owneruser/login", @@ -124,11 +127,11 @@ const frontendConfigs = { "JWT_V3_NAME":"v3jwt", "JWT_V2_NAME":"tcjwt", "COOKIES_SECURE":false, - "TC_LOGIN_URL": "https://accounts.topcoder-dev.com/member", + "TC_LOGIN_URL": "https://accounts-auth0.topcoder-dev.com", "TC_USER_PROFILE_URL": "https://api.topcoder-dev.com/v2/user/profile", "API_URL": "https://api.topcoder-qa.com", "ADMIN_TOOL_URL": "https://api.topcoder-qa.com/v2", - "ACCOUNTS_CONNECTOR_URL": "https://accounts.topcoder-qa.com/connector.html", + "ACCOUNTS_CONNECTOR_URL": "https://accounts-auth0.topcoder-dev.com", "DIRECT_URL_BASE": "https://www.topcoder-dev.com/direct/projectOverview?formData.projectId=", "OWNER_LOGIN_GITHUB_URL":"/api/v1/github/owneruser/login", "OWNER_LOGIN_GITLAB_URL":"/api/v1/gitlab/owneruser/login", @@ -140,11 +143,11 @@ const frontendConfigs = { "JWT_V3_NAME":"v3jwt", "JWT_V2_NAME":"tcjwt", "COOKIES_SECURE":false, - "TC_LOGIN_URL": "https://accounts.topcoder.com/member", + "TC_LOGIN_URL": "https://accounts-auth0.topcoder.com", "TC_USER_PROFILE_URL": "https://api.topcoder.com/v2/user/profile", "API_URL": "https://api.topcoder.com", "ADMIN_TOOL_URL": "https://api.topcoder.com/v2", - "ACCOUNTS_CONNECTOR_URL": "https://accounts.topcoder.com/connector.html", + "ACCOUNTS_CONNECTOR_URL": "https://accounts-auth0.topcoder.com", "DIRECT_URL_BASE": "https://www.topcoder.com/direct/projectOverview?formData.projectId=", "OWNER_LOGIN_GITHUB_URL":"/api/v1/github/owneruser/login", "OWNER_LOGIN_GITLAB_URL":"/api/v1/gitlab/owneruser/login", @@ -173,4 +176,4 @@ module.exports.frontendConfigs = { TOPCODER_URL: process.env.TOPCODER_URL || frontendConfigs[activeEnv].TOPCODER_URL, GITHUB_TEAM_URL: process.env.GITHUB_TEAM_URL || frontendConfigs[activeEnv].GITHUB_TEAM_URL, GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || frontendConfigs[activeEnv].GITLAB_GROUP_URL -}; \ No newline at end of file +}; diff --git a/src/front/src/app/auth/auth.config.js b/src/front/src/app/auth/auth.config.js index 68380ab..818ca22 100644 --- a/src/front/src/app/auth/auth.config.js +++ b/src/front/src/app/auth/auth.config.js @@ -9,25 +9,6 @@ angular.module('topcoderX') .config(['$httpProvider', 'jwtInterceptorProvider', function ($httpProvider, jwtInterceptorProvider) { - var refreshingToken = null; - - function handleRefreshResponse(res, $authService) { - var ref; - var ref1; - var ref2; - - const newToken = (ref = res.data) != null ? (ref1 = ref.result) != null ? - (ref2 = ref1.content) != null ? ref2.token : void 0 : void 0 : void 0; - - $authService.setTokenV3(newToken); - - return newToken; - }; - - function refreshingTokenComplete() { - refreshingToken = null; - }; - jwtInterceptorProvider.tokenGetter = [ 'AuthService', '$http', 'Helper', '$rootScope', 'config', function (AuthService, $http, Helper, $rootScope, config) { @@ -43,18 +24,9 @@ angular.module('topcoderX') var currentToken = AuthService.getTokenV3(); if (AuthService.getTokenV3() && AuthService.isTokenV3Expired()) { - if (refreshingToken === null) { - refreshingToken = $http({ - method: 'GET', - url: $rootScope.appConfig.API_URL + "/v3/authorizations/1", - headers: { - 'Authorization': "Bearer " + currentToken - } - }).then(function (res) { handleRefreshResponse(res, AuthService) })["finally"](refreshingTokenComplete).catch(function () { - AuthService.login(); - }); - } - return refreshingToken; + var token = AuthService.getToken('v3jwt') + if (token) return token + else AuthService.login() } else { return currentToken; } diff --git a/src/front/src/app/auth/auth.service.js b/src/front/src/app/auth/auth.service.js index 14a6423..acdec7a 100644 --- a/src/front/src/app/auth/auth.service.js +++ b/src/front/src/app/auth/auth.service.js @@ -4,19 +4,9 @@ angular.module('topcoderX') .factory('AuthService', [ '$q', '$log', 'jwtHelper', '$cookies', '$window', '$state', '$rootScope', '$http', 'Helper', function ($q, $log, jwtHelper, $cookies, $window, $state, $rootScope, $http, Helper) { - // these constants are for AuthService internal usage only - // they don't depend on the environment thus don't have to be placed in global config - - var GET_FRESH_TOKEN_REQUEST = 'GET_FRESH_TOKEN_REQUEST'; - var GET_FRESH_TOKEN_SUCCESS = 'GET_FRESH_TOKEN_SUCCESS'; - var GET_FRESH_TOKEN_FAILURE = 'GET_FRESH_TOKEN_FAILURE'; - - var LOGOUT_REQUEST = 'LOGOUT_REQUEST'; - var LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; - var LOGOUT_FAILURE = 'LOGOUT_FAILURE'; // local variables - var connectorIFrame, url, loading; + var connectorIFrame, loading; /** * Create invisible iframe and append it to the body @@ -49,35 +39,17 @@ angular.module('topcoderX') /** * Proxies calls to the iframe from main window * - * @param {String} REQUEST request id - * @param {String} SUCCESS success respond id - * @param {String} FAILURE failure respond id - * @param {Object} params params of the request * @return {Promise} promise of the request */ - function proxyCall(REQUEST, SUCCESS, FAILURE, params) { + function proxyCall() { if (!connectorIFrame) { throw new Error('connector has not yet been configured.') } - params = arguments.length > 3 && angular.isDefined(arguments[3]) ? arguments[3] : {}; - function request() { return $q(function (resolve, reject) { - function receiveMessage(e) { - var safeFormat = e.data.type === SUCCESS || e.data.type === FAILURE - if (safeFormat) { - window.removeEventListener('message', receiveMessage) - if (e.data.type === SUCCESS) resolve(e.data) - if (e.data.type === FAILURE) reject(e.error) - } - } - - window.addEventListener('message', receiveMessage) - - var payload = $.extend({}, { type: REQUEST }, params) - - connectorIFrame.contentWindow.postMessage(payload, url) + var token = AuthService.getToken('v3jwt') + token ? resolve({ token: token }) : reject("v3jwt cookie not found") // eslint-disable-line no-unused-expressions }) } @@ -95,7 +67,6 @@ angular.module('topcoderX') $log.warn('iframe connector can only be configured once, this request has been ignored.') } else { connectorIFrame = createFrame(options.frameId, options.connectorUrl) - url = options.connectorUrl loading = $q(function (resolve) { connectorIFrame.onload = function () { @@ -105,6 +76,29 @@ angular.module('topcoderX') } } + function fromPairs(arr) { + return arr.reduce(function(accumulator, value) { + accumulator[value[0]] = value[1]; + return accumulator; + }, {}) + } + + /** + * parse cookie to find a key data. + * + * @param {String} cookie cookie data + * @return {Object} parsed cookie + */ + function parseCookie(cookie) { + return fromPairs( + cookie + .split(';') + .map( + function (pair) { return pair.split('=').map(function(part) { return part.trim() }) } + ) + ) + } + var AuthService = { ERROR: { NO_PERMISSIONS: 'Current user doesn\'t have permissions.', @@ -112,6 +106,16 @@ angular.module('topcoderX') PermissionDenied: false, }; + /** + * Get token in cookie based on key. + * + * @param {String} key the key + * @return {Object} token data object + */ + AuthService.getToken = function(key) { + return parseCookie(document.cookie)[key] + } + /** * Returns promise which is resolved when connector iframe is loaded * @@ -132,7 +136,7 @@ angular.module('topcoderX') * @return {Promise} promise to get token v3 */ AuthService.retriveFreshToken = function () { - return proxyCall(GET_FRESH_TOKEN_REQUEST, GET_FRESH_TOKEN_SUCCESS, GET_FRESH_TOKEN_FAILURE) + return proxyCall() .then(function (data) { AuthService.setTokenV3(data.token); return AuthService.isAuthorized(); @@ -146,16 +150,9 @@ angular.module('topcoderX') * @return {Promise} promise which is resolved when user is logged out on the server */ AuthService.logout = function () { - // send request to the server that we want to log out - // save loggingOut promise to be accessed any time - AuthService.logginOut = proxyCall(LOGOUT_REQUEST, LOGOUT_SUCCESS, LOGOUT_FAILURE).then(function () { - AuthService.logginOut = null; - // remove only token V3, which we set from the script manually - // token V2 will be removed automatically during logout server request - $cookies.remove($rootScope.appConfig.JWT_V3_NAME, { path: '/' }); - }); - - return AuthService.logginOut; + $cookies.remove($rootScope.appConfig.JWT_V3_NAME, { path: '/' }); + $window.location.href = $rootScope.appConfig.TC_LOGIN_URL + '?logout=true&retUrl=' + encodeURIComponent($window.location.href); + // return AuthService.logginOut; } AuthService.login = function () { @@ -271,6 +268,29 @@ angular.module('topcoderX') } var currentUser = jwtHelper.decodeToken(tctV3); + + Object.keys(currentUser).findIndex(function (key) { + if (key.includes('roles')) { + currentUser.roles = currentUser[key]; + return true; + } + return false; + }); + Object.keys(currentUser).findIndex(function (key) { + if (key.includes('handle')) { + currentUser.handle = currentUser[key]; + return true; + } + return false; + }); + Object.keys(currentUser).findIndex(function (key) { + if (key.includes('userId')) { + currentUser.userId = parseInt(currentUser[key], 10); + return true; + } + return false; + }); + currentUser.id = currentUser.userId; currentUser.token = tctV3; @@ -289,7 +309,6 @@ angular.module('topcoderX') $rootScope.appConfig = res.data; if (connectorIFrame && !connectorIFrame.src) { connectorIFrame.src = $rootScope.appConfig.ACCOUNTS_CONNECTOR_URL; - url = $rootScope.appConfig.ACCOUNTS_CONNECTOR_URL; } return $q.resolve(res.data); }).catch(function (err) { diff --git a/src/front/src/app/main/main.controller.js b/src/front/src/app/main/main.controller.js index 3205db0..6b29f0d 100644 --- a/src/front/src/app/main/main.controller.js +++ b/src/front/src/app/main/main.controller.js @@ -56,7 +56,6 @@ angular.module('topcoderX') $scope.logout = function () { AuthService.logout(); - $state.go('auth'); }; // auth diff --git a/src/front/src/components/common/navigation.controller.js b/src/front/src/components/common/navigation.controller.js index 5a1636a..9c7ed28 100644 --- a/src/front/src/components/common/navigation.controller.js +++ b/src/front/src/components/common/navigation.controller.js @@ -1,28 +1,28 @@ 'use strict'; angular.module('topcoderX') // eslint-disable-line angular/no-services - .controller('NavController', ['$scope', '$log', '$state', '$cookies', '$http', '$rootScope', - function ($scope, $log, $state, $cookies, $http, $rootScope) { + .controller('NavController', ['$scope', '$log', '$state', '$cookies', 'jwtHelper', '$rootScope', + function ($scope, $log, $state, $cookies, jwtHelper, $rootScope) { $scope.$state = $state; $scope.menuList = false; $scope.user = {}; $scope.appConfig = $rootScope.appConfig; const token = $cookies.get('tcjwt'); - const req = { - url: $rootScope.appConfig.TC_USER_PROFILE_URL, - method: 'Get', - headers: { - Authorization: 'Bearer ' + token, - }, - }; - $http(req).then(function (tcUser) { - $scope.user = tcUser.data; - if (tcUser.data.copilot) { - $log.info('Success - user is a copilot'); - } else { - $log.warn('Warning - User isn\'t a a copilot'); + const decodedToken = jwtHelper.decodeToken(token); + $scope.user = {}; + $scope.user['copilot'] = false; + Object.keys(decodedToken).findIndex(function (key) { + if (key.includes('roles')) { + if (key.indexOf('copilot') > -1) { + $scope.user['copilot'] = true; + $log.info('User is a copilot'); + } else { + $log.info('user is not a copilot'); + } + return true; } + return false; }); $scope.forceStateProjects = function () { diff --git a/src/front/src/components/common/topnavbar.html b/src/front/src/components/common/topnavbar.html index 24787cd..e59d014 100644 --- a/src/front/src/components/common/topnavbar.html +++ b/src/front/src/components/common/topnavbar.html @@ -16,7 +16,7 @@
  • - + Sign Out diff --git a/src/services/TCUserService.js b/src/services/TCUserService.js index ddd31bd..4c7d556 100644 --- a/src/services/TCUserService.js +++ b/src/services/TCUserService.js @@ -9,29 +9,27 @@ * @version 1.0 */ const Joi = require('joi'); -const superagent = require('superagent'); -const superagentPromise = require('superagent-promise'); -const config = require('../config'); +const decodeToken = require('@topcoder-platform/tc-auth-lib').decodeToken; const errors = require('../common/errors'); const helper = require('../common/helper'); const UserMapping = require('../models').UserMapping; -const request = superagentPromise(superagent, Promise); - - /** * gets the handle of tc user. * @param {String} token the user token * @returns {String} the handle */ async function getHandle(token) { - const handle = await request + //issue - https://github.com/topcoder-platform/topcoder-x-ui/issues/342 + + /* const handle = await request .get(config.TOPCODER_VALUES[config.TOPCODER_ENV].TC_USER_PROFILE_URL) .set('Authorization', `Bearer ${token}`) .end() .then((res) => res.body.handle); - - return handle; + */ + const decoded = decodeToken(token); + return decoded.handle; } getHandle.schema = Joi.object().keys({