diff --git a/.env b/.env index 50bbb13e35..b03b378409 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ REACT_APP_API_STUDY_SERVER=api/study-server REACT_APP_API_CASE_SERVER=api/case-server + +REACT_APP_USE_AUTHENTICATION=true diff --git a/.env.development b/.env.development new file mode 100644 index 0000000000..2f1d4e1a45 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +REACT_APP_USE_AUTHENTICATION=false diff --git a/package-lock.json b/package-lock.json index ae9c24279e..9c3b98eb37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4536,6 +4536,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "css": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", @@ -10102,6 +10107,24 @@ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "oidc-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.10.1.tgz", + "integrity": "sha512-/QB5Nl7c9GmT9ir1E+OVY3+yZZnuk7Qa9ZEAJqSvDq0bAyAU9KAgeKipTEfKjGdGLTeOLy9FRWuNpULMkfZydQ==", + "requires": { + "base64-js": "^1.3.0", + "core-js": "^2.6.4", + "crypto-js": "^3.1.9-1", + "uuid": "^3.3.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + } + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 59a61bf3da..ca3006e916 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react-window": "^1.8.5", "redux": "^4.0.5", "typeface-roboto": "0.0.75", + "oidc-client": "^1.10.1", "react-virtualized": "^9.21.2" }, "scripts": { diff --git a/public/idpSettings.json b/public/idpSettings.json new file mode 100644 index 0000000000..8423e24097 --- /dev/null +++ b/public/idpSettings.json @@ -0,0 +1,7 @@ +{ + "authority" : "#", + "client_id" : "#", + "redirect_uri": "#", + "post_logout_redirect_uri" : "#", + "scope" : "#" +} diff --git a/src/components/app.js b/src/components/app.js index 2252620bff..0a98b5ce6f 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -5,9 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from 'react'; +import React, {useEffect, useState} from 'react'; -import {useSelector} from 'react-redux' +import {useDispatch, useSelector} from 'react-redux' import {Route, Switch, useHistory, useLocation} from 'react-router-dom'; @@ -18,6 +18,8 @@ import StudyManager from './study-manager'; import TopBar from './top-bar'; import {LIGHT_THEME} from '../redux/actions' import Parameters from "./parameters"; +import {userManagerPromise, login, logout, handleSigninCallback} from '../utils/authentication/AuthService'; +import Authentication from "./authentication"; const lightTheme = createMuiTheme({ palette: { @@ -41,23 +43,50 @@ const getMuiTheme = (theme) => { } }; -const App = () => { +const SignInCallback = (props) => { + useEffect(() => { + if (props.userManager.instance !== null) { + props.handleSigninCallback(); + } + }, [props.userManager]); + + return ( +

+ ) +}; +const noUserManager = {instance: null, error: null}; + +const App = () => { const theme = useSelector(state => state.theme); + const user = useSelector(state => state.user); + + const [userManager, setUserManager] = useState(noUserManager); + const history = useHistory(); + const dispatch = useDispatch(); + const location = useLocation(); + useEffect(() => { + userManagerPromise + .then(userManager => { + setUserManager({instance : userManager, error : null }); + }) + .catch(function(error) { + setUserManager({instance : null, error : error.message}); + console.debug("error when importing the idp settings") + }); + }, []); + function studyClickHandler(studyName) { history.push("/studies/" + studyName); } function showParameters() { - if (location.pathname === "/parameters") { - // if already at parameters go back to study - history.goBack(); - } else { + if (location.pathname !== "/parameters") { history.push("/parameters"); } } @@ -66,21 +95,33 @@ const App = () => { - showParameters() }/> - - - studyClickHandler(name) }/> - - - - - - - - -

Error: bad URL; No matched Route.

-
-
+ showParameters()} onLogoutClick={() => logout(dispatch, userManager.instance)}/> + { user !== null ? ( + + + studyClickHandler(name)}/>) + + + + + + + + +

Error: bad URL; No matched Route.

+
+
) + : ( + + + handleSigninCallback(dispatch, history, userManager.instance)}/> + + + {userManager.error !== null && (

Error : Getting userManager; {userManager.error}

)} + {userManager.error === null && ( login(location, userManager.instance)}/>)} +
+
+ )}
) diff --git a/src/components/authentication.js b/src/components/authentication.js new file mode 100644 index 0000000000..780c7dfa4d --- /dev/null +++ b/src/components/authentication.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2020, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import React, {useEffect} from 'react'; +import Avatar from '@material-ui/core/Avatar'; +import Button from '@material-ui/core/Button'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Link from '@material-ui/core/Link'; +import Box from '@material-ui/core/Box'; +import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; +import Container from '@material-ui/core/Container'; +import {FormattedMessage} from "react-intl"; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + submit: { + margin: theme.spacing(3, 0, 2), + borderRadius: '30px' + }, + logo: { + width: 64, + height: 64, + }, +})); + + +const Authentication = (props) => { + const classes = useStyles(); + + function Copyright() { + return ( + + {'Copyright © '} + + Grid Suite + {' '} + {new Date().getFullYear()} + {'.'} + + ); + } + + return ( + + +
+ + + + + ? + + + +
+ + + +
+ ); +}; + +export default Authentication; diff --git a/src/components/study-manager.js b/src/components/study-manager.js index ec93b2c3bd..eec698a13c 100644 --- a/src/components/study-manager.js +++ b/src/components/study-manager.js @@ -34,6 +34,7 @@ import {ReactComponent as IeeeLogo} from '../images/ieee_logo.svg'; import {loadStudiesSuccess} from '../redux/actions'; import {fetchStudies, deleteStudy} from '../utils/rest-api'; import CreateStudyForm from "./create-study-form"; + import {CardHeader} from "@material-ui/core"; import IconButton from "@material-ui/core/IconButton"; import MoreVertIcon from '@material-ui/icons/MoreVert'; @@ -248,7 +249,6 @@ const StudyCard = ({study, onClick}) => { }; const StudyManager = ({onStudyClick}) => { - const dispatch = useDispatch(); useEffect(() => { @@ -265,12 +265,12 @@ const StudyManager = ({onStudyClick}) => { return ( - + { studies.map(study => - onStudyClick(study.studyName)} /> + onStudyClick(study.studyName)}/> ) } diff --git a/src/components/top-bar.js b/src/components/top-bar.js index 84557ee2d3..4146ad4892 100644 --- a/src/components/top-bar.js +++ b/src/components/top-bar.js @@ -11,14 +11,24 @@ import {FormattedMessage} from "react-intl"; import {useHistory} from 'react-router-dom'; import AppBar from "@material-ui/core/AppBar"; -import IconButton from "@material-ui/core/IconButton"; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; + import {makeStyles} from "@material-ui/core/styles"; import Toolbar from "@material-ui/core/Toolbar"; import Typography from "@material-ui/core/Typography"; import SettingsIcon from '@material-ui/icons/Settings'; +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuIcon from '@material-ui/icons/Menu'; +import { withStyles } from '@material-ui/core/styles'; + +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; import {ReactComponent as PowsyblLogo} from "../images/powsybl_logo.svg"; import PropTypes from "prop-types"; +import {useSelector} from "react-redux"; const useStyles = makeStyles(() => ({ grow: { @@ -35,13 +45,56 @@ const useStyles = makeStyles(() => ({ } })); +const StyledMenu = withStyles({ + paper: { + border: '1px solid #d3d4d5', + },})(props => ( + +)); + +const StyledMenuItem = withStyles(theme => ({ + root: { + '&:focus': { + backgroundColor: theme.palette.primary.main, + '& .MuiListItemIcon-root, & .MuiListItemText-primary': { + color: theme.palette.common.white, + }, + }, + }, +}))(MenuItem); + const TopBar = (props) => { + const user = useSelector(state => state.user); + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = event => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; const history = useHistory(); + const onParametersClick = () => { + handleClose(); if (props.onParametersClick) { props.onParametersClick(); } @@ -58,10 +111,45 @@ const TopBar = (props) => { -
- - - +
+

{user !== null ? user.profile.name : ""}

+ + {user && ( +
+ + + + + + + + + + + + + + + + + + + + +
+ )} ) diff --git a/src/redux/actions.js b/src/redux/actions.js index ba114b3d80..14c43843a4 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -78,3 +78,9 @@ export const USE_NAME = 'USE_NAME'; export function toggleUseNameState() { return { type: USE_NAME }; } + +export const USER = 'USER'; + +export function setLoggedUser(user) { + return { type: USER, user : user}; +} diff --git a/src/redux/reducer.js b/src/redux/reducer.js index f9f50eb46b..86ea34c586 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -26,7 +26,8 @@ import { SELECT_CASE, SELECT_FILE, SELECT_THEME, - USE_NAME + USE_NAME, + USER } from "./actions"; const initialState = { @@ -38,7 +39,8 @@ const initialState = { cases : [], selectedCase : null, selectedFile : null, - useName : getLocalStorageUseName() + useName : getLocalStorageUseName(), + user : null, }; export const reducer = createReducer(initialState, { @@ -94,4 +96,8 @@ export const reducer = createReducer(initialState, { state.useName = !state.useName; saveLocalStorageUseName(state.useName); }, + + [USER]: (state, action) => { + state.user = action.user; + }, }); diff --git a/src/translations/en.json b/src/translations/en.json index 328e38f742..01c6e4b1a7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -15,6 +15,12 @@ "delete" : "Delete", "filter" : "Filter", + + "settings" : "Settings", + "logout" : "Logout", + "login" : "Login", + "connection": "Connection", + "deleteStudy" : "Delete Study", "deleteStudyMsg" : "Are you sure you want to delete the study?", "studyNameDidNotMatchMsg" : "The given study name did not match with ", diff --git a/src/translations/fr.json b/src/translations/fr.json index 80169beca0..02811781ca 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -15,6 +15,11 @@ "delete" : "Supprimer", "filter" : "Filtre", + "settings" : "Paramètres", + "logout" : "Se déconnecter", + "login" : "Se connecter", + "connection": "Connexion", + "deleteStudy" : "Supprimer l'étude", "deleteStudyMsg" : "Etes vous sur de vouloir supprimer l'étude?", "studyNameDidNotMatchMsg" : "Le nom d'étude donné ne correspond pas à ", diff --git a/src/utils/authentication/AuthService.js b/src/utils/authentication/AuthService.js new file mode 100644 index 0000000000..c0b8dcea70 --- /dev/null +++ b/src/utils/authentication/AuthService.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2020, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import {UserManager} from 'oidc-client'; +import {UserManagerMock} from "./UserManagerMock"; +import {setLoggedUser} from "../../redux/actions"; +let userManagerPromise; +if (process.env.REACT_APP_USE_AUTHENTICATION === "true") { + userManagerPromise = fetch('idpSettings.json') + .then(r => r.json()) + .then(idpSettings => { + let settings = { + authority: idpSettings.authority, + client_id: idpSettings.client_id, + redirect_uri: idpSettings.redirect_uri, + post_logout_redirect_uri: idpSettings.post_logout_redirect_uri, + response_mode: 'fragment', + response_type: 'id_token token', + scope: idpSettings.scope, + }; + return new UserManager(settings); + }); +} else { + userManagerPromise = Promise.resolve( new UserManagerMock({})); +} + +const pathKey = "powsybl-study-app-current-path"; + +function login(location, userManagerInstance) { + sessionStorage.setItem(pathKey, location.pathname + location.search); + return userManagerInstance.signinRedirect().then(() => console.debug("login")); +} + +function logout(dispatch, userManagerInstance) { + dispatch(setLoggedUser(null)); + return userManagerInstance.signoutRedirect().then( + () => console.debug("logged out")); +} + +function dispatchUser(dispatch, userManagerInstance) { + return userManagerInstance.getUser().then(user => { + if (user) { + console.debug('User has been successfully loaded from store.'); + return dispatch(setLoggedUser(user)); + } else { + console.debug('You are not logged in.'); + } + }); +} + +function handleSigninCallback(dispatch, history, userManagerInstance) { + userManagerInstance.signinRedirectCallback().then(function () { + dispatchUser(dispatch, userManagerInstance); + const previousPath = sessionStorage.getItem(pathKey); + history.push(previousPath); + }).catch(function (e) { + console.error(e); + }); +} + +export {userManagerPromise, login, logout, handleSigninCallback} diff --git a/src/utils/authentication/UserManagerMock.js b/src/utils/authentication/UserManagerMock.js new file mode 100644 index 0000000000..c6000e4175 --- /dev/null +++ b/src/utils/authentication/UserManagerMock.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2020, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +export class UserManagerMock { + settings; + + constructor(settings) { + this.settings = settings; + } + + getUser() { + return Promise.resolve(JSON.parse(sessionStorage.getItem("powsybl-study-app-mock-user"))); + } + + signinRedirect() { + window.location = "./sign-in-callback"; + return Promise.resolve(null); + } + + signoutRedirect() { + sessionStorage.setItem("powsybl-study-app-mock-user", null); + window.location = "."; + return Promise.resolve(null); + } + signinRedirectCallback() { + sessionStorage.setItem("powsybl-study-app-mock-user", JSON.stringify( + { + profile:{ name:"John Doe"}, + id_token : "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IllNRUxIVDBndmIwbXhvU0RvWWZvbWpxZmpZVSJ9.eyJhdWQiOiI5YzQwMjQ2MS1iMmFiLTQ3NjctOWRiMy02Njg1OWJiMGZjZDAiLCJpc3MiOiJodHRwczovL2" + + "xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzUwMmRhZDUtZDY0Yy00NmM3LTlkNDctYjE2ZjU4MGZjZmE5L3YyLjAiLCJpYXQiOjE1ODUzMzEyNDksIm5iZiI6MTU4NTMzMTI0OSwiZXhwIjoxNTg1MzM1MTQ5LCJhaW8iOiJBV1FB" + + "bS84UEFBQUF3Q0xyTDRIUEUvTnVjOU9OdHN0SUV4cVpyMUlqa1FGbXJvUW5EUzJBaksyWnpneUhQTldPdkE3bitveHkvRzgxWElsb1A0TitsQjZINFJteElwakhNYVArTjIyTzVnMUFaR04yc1d6VHA5T3JWMDIvOXhndXJBMjZrdU" + + "NXbGg2RSIsImF0X2hhc2giOiJJaWRYdGRHdzVkbjlOZDFQblVvbDh3IiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkLyIsIm5vbmNlIjoiMjkzZTcxNzhm" + + "OWE5NGZlNjg1ZWY3MjdlZTg5MTYxYjEiLCJzdWIiOiJyTnZjWXJMSXJSN25iSDJPQlhoOFkzU05wZEtPc3dfTUNkX3F3NF9vNDRJIiwidGlkIjoiNzUwMmRhZDUtZDY0Yy00NmM3LTlkNDctYjE2ZjU4MGZjZmE5IiwidXRpIjoiUF" + + "BYdkw1UWxDMG1oMGp2N3NaNGJBQSIsInZlciI6IjIuMCJ9.dPAh24KTfsqmDaRoBtMLcayAWnDqVtydQ97P1a99dg93JsDu4Jhxju9vlzvjd6Ro5a1RZdrKFKB_pgC2DkQ3wSeYjpdSNyBAlW1_ryq65JkTJVMp33OsM_7SdjaRIiJ" + + "fPiJ3U9jRBSyj7ofoHCLUjD_Uu-XreKxpMGhFHOQIO72UfXg8TBpsapjkEv9Dyz2UqMa2BQvO5mxKw93LNg5BI6j2a5LhbMEmmRWqfxWGITJ9TWfHjYdFkrXKcmvWZ9D2b4tsw_5NorDxkuzVFhA89M_0ASzOXoj1Yb6LgdkzWXDim" + + "ssvyyz5Oe4V3gdkAe8Jj7Uwz-9AR-MO2kNkH7ytHA", + session_state : "session state", + access_token : "eyJ0eXAiOiJKV1QiLCJub25jZSI6InhKWHlQeXVrU1paQ3BOeEcxZUQway1lVDF0YzZtQ01ZVkZKcnBDOTJxc28iLCJhbGciOiJSUzI1NiIsIng1dCI6IllNRUxIVDBndmIwbXhvU0RvWWZvbWpxZmpZVSIsImtpZC" + + "I6IllNRUxIVDBndmIwbXhvU0RvWWZvbWpxZmpZVSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83NTAyZGFkNS1kNjRjLTQ2YzctOWQ" + + "0Ny1iMTZmNTgwZmNmYTkvIiwiaWF0IjoxNTg1MzMxMjQ5LCJuYmYiOjE1ODUzMzEyNDksImV4cCI6MTU4NTMzNTE0OSwiYWNjdCI6MCwiYWNyIjoiMSIsImFpbyI6IkFVUUF1LzhQQUFBQXdwc3RYMlVkY2VDQWx4dU9tVHpIY0R3R" + + "lhTWUtYanIvZUNTSi9PdTRqbTJyUVBCUml0U1dWMThmNldCVEdNdnQ5ZGx0Ry9lTXB1VXZqaTN2NCtzanh3PT0iLCJhbHRzZWNpZCI6IjE6bGl2ZS5jb206MDAwMzQwMDExOUZEOTIxMiIsImFtciI6WyJwd2QiXSwiYXBwX2Rpc3B" + + "sYXluYW1lIjoic3BhIiwiYXBwaWQiOiI5YzQwMjQ2MS1iMmFiLTQ3NjctOWRiMy02Njg1OWJiMGZjZDAiLCJhcHBpZGFjciI6IjAiLCJlbWFpbCI6ImNoYW1zZWRkaW5lLmJlbmhhbWVkQGVuc2ktdW1hLnRuIiwiZmFtaWx5X25hb" + + "WUiOiJCRU5IQU1FRCIsImdpdmVuX25hbWUiOiJDaGFtc2VkZGluZSIsImlkcCI6ImxpdmUuY29tIiwiaXBhZGRyIjoiNzcuMjA0LjE0Ni4xNTkiLCJuYW1lIjoiQ2hhbXNlZGRpbmUgQkVOSEFNRUQiLCJvaWQiOiIzNTIzYmQ3OC0" + + "yZjIxLTQ3ZjYtODhlOC1hYWIzYjZmMjdmNjAiLCJwbGF0ZiI6IjE0IiwicHVpZCI6IjEwMDMyMDAwOURFMDg1NkEiLCJzY3AiOiJVc2VyLlJlYWQgcHJvZmlsZSBvcGVuaWQgZW1haWwiLCJzdWIiOiJjVEd5LVlfV3FLR2x1cmRUV" + + "DdSUVlfY3FjSDJoVHpEdllZTmotQ3hONXA4IiwidGlkIjoiNzUwMmRhZDUtZDY0Yy00NmM3LTlkNDctYjE2ZjU4MGZjZmE5IiwidW5pcXVlX25hbWUiOiJsaXZlLmNvbSNjaGFtc2VkZGluZS5iZW5oYW1lZEBlbnNpLXVtYS50biI" + + "sInV0aSI6IlBQWHZMNVFsQzBtaDBqdjdzWjRiQUEiLCJ2ZXIiOiIxLjAiLCJ4bXNfc3QiOnsic3ViIjoick52Y1lyTElyUjduYkgyT0JYaDhZM1NOcGRLT3N3X01DZF9xdzRfbzQ0SSJ9LCJ4bXNfdGNkdCI6MTU4MjgyMDM1Mn0.W_" + + "ccOGW_AGdg37KSMi7LWHtvm3Mw5p1dHjgDIrUaXduKF2iLS4dCaPw7yeo4VjAcOyV6C0h6ABLDCtkwVt8BSDTIIU7DaT8k2bRbMCCq69BmeiYPsbp-yX6ywGCx5DHsnOLqI2oHbBQktA2Nmv9Va651Pbm3OpSPuGPdVimkFCcnisiGl" + + "UOej1ZMNwyVT6386O2pERPtxmFUt_D1dKLxBXxBNxLVUG5BG3bI7wMpBOHEUA5CbaBzYXmGrLMXVVbrj9OsF-WQ6aNoqsm9cicX6pJB60lFz1dxLeSgcFO7Zh2K3PFe4FnXCqAvNPadQMz_kJEO9_phlDV85c2MPqeXbA", + token_type : "Bearer", + scope : "scopes" + + } + )); + return Promise.resolve(""); + } +}