├── .gitignore ├── env.json.example ├── electron_oidc_sample_app_banner.png ├── main ├── app-process.js ├── preload.js ├── auth-process.js └── main.js ├── renderers ├── home.js ├── home.css ├── login.js ├── login.html ├── home.html ├── login.css ├── image2.svg └── image.svg ├── services ├── store-service.js └── auth-service.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | env.json 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /env.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "DESCOPE_PROJECT_ID": "" 3 | } -------------------------------------------------------------------------------- /electron_oidc_sample_app_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/electron-oidc-sample-app/HEAD/electron_oidc_sample_app_banner.png -------------------------------------------------------------------------------- /main/app-process.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow } = require("electron"); 2 | const path = require("path"); 3 | 4 | function createAppWindow() { 5 | let win = new BrowserWindow({ 6 | width: 1000, 7 | height: 600, 8 | webPreferences: { 9 | preload: path.join(__dirname, "preload.js"), 10 | } 11 | }); 12 | 13 | win.loadFile('./renderers/home.html'); 14 | // win.webContents.openDevTools(); 15 | 16 | win.on('closed', () => { 17 | win = null; 18 | }); 19 | } 20 | 21 | module.exports = createAppWindow; -------------------------------------------------------------------------------- /main/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require("electron"); 2 | 3 | // API Definition 4 | const electronAPI = { 5 | getProfile: () => ipcRenderer.invoke('auth:get-profile'), 6 | goAuthPage: (flowParam) => ipcRenderer.send('auth:go-auth-url', flowParam), 7 | logOut: () => ipcRenderer.send('auth:log-out'), 8 | validate: () => ipcRenderer.invoke('auth:validate'), 9 | }; 10 | 11 | // Register the API with the contextBridge 12 | process.once("loaded", () => { 13 | contextBridge.exposeInMainWorld('electronAPI', electronAPI); 14 | }); -------------------------------------------------------------------------------- /renderers/home.js: -------------------------------------------------------------------------------- 1 | addEventListener('load',async () =>{ 2 | const valid = await window.electronAPI.validate(); // Validates Token on Refresh 3 | if(valid){ 4 | const profile = await window.electronAPI.getProfile(); 5 | document.getElementById('picture').src = profile.picture; 6 | document.getElementById('name').innerText = profile.name; 7 | document.getElementById('success').innerText = 'You successfully used OpenID Connect and OAuth 2.0 to authenticate.'; 8 | } 9 | }); 10 | 11 | document.getElementById('logout').onclick = () => { 12 | window.electronAPI.logOut(); 13 | }; 14 | -------------------------------------------------------------------------------- /renderers/home.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | padding-bottom: 30px; 4 | } 5 | 6 | div.profile { 7 | display: grid; 8 | grid-template-columns: 1fr auto auto auto; 9 | align-items: start; 10 | width: 100%; 11 | } 12 | 13 | div.profile span { 14 | display: inline-block; 15 | align-self: center; 16 | color: #ccc; 17 | margin-left: 10px; 18 | margin-right: 25px; 19 | } 20 | 21 | div.profile img { 22 | width: 30px; 23 | border-radius: 50%; 24 | align-self: center; 25 | } 26 | 27 | div#message { 28 | display: none; 29 | } -------------------------------------------------------------------------------- /services/store-service.js: -------------------------------------------------------------------------------- 1 | const { safeStorage } = require("electron"); 2 | const settings = require("electron-settings"); 3 | 4 | async function storeToken(token_name, token) { 5 | const encrypted_token = safeStorage.encryptString(token); 6 | await settings.set(token_name, encrypted_token); 7 | } 8 | 9 | async function retrieveToken(token_name) { 10 | const token = await settings.get(token_name); 11 | const decrypted_token = safeStorage.decryptString(Buffer.from(token.data)); 12 | return decrypted_token; 13 | } 14 | 15 | module.exports = { 16 | storeToken, 17 | retrieveToken 18 | } -------------------------------------------------------------------------------- /renderers/login.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | document.getElementById('signInButton').onclick = () => { 5 | try { 6 | window.electronAPI.goAuthPage("sign-in"); 7 | 8 | if (!document.querySelector('.success-message')) { 9 | const message = document.createElement('p'); 10 | message.textContent = 'A browser was opened for you'; 11 | message.className = 'success-message'; 12 | 13 | const container = document.querySelector('.container'); 14 | const footer = document.querySelector('.footer'); 15 | container.insertBefore(message, footer); 16 | } 17 | } catch (error) { 18 | console.log("Error signing in :",error) 19 | 20 | } 21 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-oidc-sample-app", 3 | "version": "1.0.0", 4 | "description": "Sample Electron app showing signing up or in using browser", 5 | "keywords": [], 6 | "author": "", 7 | "license": "ISC", 8 | "main": "./main/main.js", 9 | "scripts": { 10 | "dist": "electron-builder" 11 | }, 12 | "build": { 13 | "appId": "electron-oidc-sample-app", 14 | "dmg": { 15 | "title": "${productName} ${version}" 16 | }, 17 | "linux": { 18 | "target": [ 19 | "AppImage", 20 | "deb" 21 | ] 22 | }, 23 | "win": { 24 | "target": "NSIS", 25 | "icon": "build/icon.ico" 26 | } 27 | }, 28 | "devDependencies": { 29 | "electron": "^32.0.2", 30 | "electron-builder": "^25.0.5" 31 | }, 32 | "dependencies": { 33 | "axios": "^1.7.7", 34 | "electron-settings": "^4.0.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /renderers/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login 7 | 8 | 9 | 10 | 11 |
12 | 16 |

Welcome to sample-app

17 |
18 | 19 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Descope Sample Apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main/auth-process.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow, shell, app, dialog } = require('electron'); 2 | const authService = require('../services/auth-service'); 3 | const path = require("path"); 4 | 5 | let win = null; 6 | 7 | function createAuthWindow() { 8 | destroyAuthWin(); 9 | 10 | win = new BrowserWindow({ 11 | width: 1000, 12 | height: 600, 13 | webPreferences: { 14 | preload: path.join(__dirname, "preload.js"), 15 | enableRemoteModule: false 16 | } 17 | }); 18 | 19 | win.loadFile('./renderers/login.html'); 20 | 21 | win.on('authenticated', () => { 22 | destroyAuthWin(); 23 | }); 24 | 25 | win.on('closed', () => { 26 | win = null; 27 | }); 28 | } 29 | 30 | async function goAuthUrl(flowParam) { 31 | try { 32 | const auth_url = await authService.getAuthenticationURL(flowParam); 33 | shell.openExternal(auth_url) // Open OIDC Authorize URL in External Browser 34 | } catch (error) { 35 | console.error("Could not open Auth URL:", error) 36 | } 37 | } 38 | 39 | function destroyAuthWin() { 40 | if (!win) return; 41 | win.close(); 42 | win = null; 43 | } 44 | 45 | function createLogoutWindow() { 46 | createAuthWindow(); 47 | authService.logout() 48 | .then() 49 | } 50 | 51 | module.exports = { 52 | createAuthWindow, 53 | createLogoutWindow, 54 | goAuthUrl 55 | }; -------------------------------------------------------------------------------- /renderers/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sample-app 9 | 15 | 16 | 17 | 18 | 19 | 27 |
28 |
29 |
30 |

Electron App

31 |

32 | This Electron application is secured with OpenID Connect and OAuth 33 | 2.0. 34 |

35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /renderers/login.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | height: 100vh; 8 | background-color: #1C1C1C; 9 | color: #FFFFFF; 10 | font-family: Arial, sans-serif; 11 | } 12 | 13 | .container { 14 | text-align: center; 15 | max-width: 400px; 16 | width: 100%; 17 | } 18 | 19 | .logo { 20 | margin-bottom: 40px; 21 | } 22 | 23 | h1 { 24 | font-size: 24px; 25 | margin-bottom: 20px; /* Reduced margin to make space for the divider */ 26 | } 27 | 28 | .divider { 29 | width: 100%; 30 | height: 1px; 31 | background-color: #6c757d; /* Grey color for the divider */ 32 | margin-bottom: 20px; 33 | } 34 | 35 | button { 36 | width: 100%; 37 | padding: 18px; /* Increased padding to make buttons slightly longer */ 38 | margin: 10px 0; 39 | font-size: 16px; 40 | border: none; 41 | border-radius: 15px; /* Less rounded corners */ 42 | cursor: pointer; 43 | transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; 44 | } 45 | 46 | .sign-in { 47 | background-color: #FFFFFF; 48 | color: #000000; 49 | } 50 | 51 | .sign-in:hover { 52 | background-color: #f2f2f2; /* Slightly darker on hover */ 53 | } 54 | 55 | .create-account { 56 | background-color: #000000; 57 | color: #FFFFFF; 58 | border: 1px solid #FFFFFF; /* Thin white border */ 59 | } 60 | 61 | .create-account:hover { 62 | background-color: #333333; /* Slightly lighter black on hover */ 63 | border-color: #f2f2f2; /* Slightly darker border on hover */ 64 | } 65 | 66 | .footer { 67 | margin-top: 50px; 68 | font-size: 12px; 69 | color: #999999; 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Descope Electron OIDC Banner 2 | 3 | # Electron + Descope OIDC Sample App 4 | 5 | This app showcases how to implement OIDC into any Electron application with Descope, allowing for seamless and secure authentication in your desktop app. This app is written in basic HTML, JS, and CSS, so it's great for all experience levels. 6 | 7 | ## Features 8 | 9 | - **Secure Authentication**: Utilizes Descope for user authentication. 10 | - **Customizable Experience**: Easy-to-change authentication flows for quick implementation. 11 | 12 | ## Getting Started 13 | Follow these steps to clone the repository and start using the app. 14 | 15 | ### Prerequisites 16 | 17 | - An account on [Descope](https://descope.com/). 18 | 19 | ### Clone the Repository 20 | 21 | Start by cloning the repository to your local machine: 22 | 23 | ```bash 24 | git clone https://github.com/descope-sample-apps/electron-oidc-sample-app.git 25 | cd electron-oidc-sample-app 26 | ``` 27 | ### Configuration 28 | 29 | First, change `env.json.example` to `env.json`. Then add your [Descope Project ID](https://app.descope.com/settings/project) to `env.json`. 30 | 31 | ``` 32 | { 33 | "DESCOPE_PROJECT_ID": "" 34 | } 35 | ``` 36 | 37 | Second, install packages, and build the Electron app: 38 | 39 | ```bash 40 | npm i 41 | npm run dist 42 | ``` 43 | 44 | After the app finishes building you can open and run your application in the `dist` folder that was created. 45 | 46 | 47 | ## Learn More 48 | To learn more please see the [Descope Documentation and API reference page](https://docs.descope.com/). 49 | 50 | ## Contact Us 51 | If you need help you can [contact us](https://docs.descope.com/support/) 52 | 53 | ## License 54 | The Electron OIDC Sample App is licensed for use under the terms and conditions of the MIT license Agreement. 55 | -------------------------------------------------------------------------------- /main/main.js: -------------------------------------------------------------------------------- 1 | const { app, ipcMain, BrowserWindow } = require("electron"); 2 | const path = require("node:path"); 3 | const { createAuthWindow, createLogoutWindow, goAuthUrl} = require("./auth-process"); 4 | const createAppWindow = require("./app-process"); 5 | const authService = require("../services/auth-service"); 6 | 7 | if (process.defaultApp) { 8 | // Setup the custom protocol 9 | if (process.argv.length >= 2) { 10 | app.setAsDefaultProtocolClient("electron", process.execPath, [ 11 | path.resolve(process.argv[1]), 12 | ]); 13 | } 14 | } else { 15 | app.setAsDefaultProtocolClient("electron"); 16 | } 17 | 18 | async function showWindow() { 19 | // Which window to show based on wether valid refresh token exists 20 | try { 21 | await authService.refreshTokens(); 22 | createAppWindow(); 23 | } catch (err) { 24 | createAuthWindow(); 25 | } 26 | } 27 | 28 | app.on("ready", () => { 29 | // Handle IPC messages from the renderer process. 30 | ipcMain.handle("auth:get-profile", authService.getProfile); 31 | ipcMain.on("auth:go-auth-url", async (event, flowParam) => { 32 | await goAuthUrl(flowParam); 33 | }); 34 | ipcMain.handle("auth:validate", async () => { 35 | const valid = await authService.validateSession(); 36 | if (!valid) { 37 | BrowserWindow.getAllWindows().forEach((window) => window.close()); 38 | createLogoutWindow(); 39 | createAuthWindow(); 40 | return false; 41 | } else { 42 | return true; 43 | } 44 | }); 45 | ipcMain.on("auth:log-out", () => { 46 | BrowserWindow.getAllWindows().forEach((window) => window.close()); 47 | createLogoutWindow(); 48 | }); 49 | // Show window after all listeners have been set and app is ready 50 | showWindow(); 51 | }); 52 | 53 | app.on("open-url", (event, url) => { 54 | // Listens for app protocol link to be opened and gets custom protocol URL 55 | authService.loadTokens(url) 56 | .then(() => { 57 | BrowserWindow.getAllWindows().forEach((window) => window.close()); 58 | createAppWindow(); 59 | }) 60 | .catch((error) => { 61 | console.error("Error loading tokens:", error); 62 | }); 63 | }); 64 | 65 | // Quit when all windows are closed. 66 | app.on("window-all-closed", () => { 67 | app.quit(); 68 | }); 69 | -------------------------------------------------------------------------------- /renderers/image2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /renderers/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 35 | 40 | 45 | 49 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /services/auth-service.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const url = require("url"); 3 | const {storeToken, retrieveToken} = require("./store-service") 4 | const env = require("../env"); 5 | 6 | const { DESCOPE_PROJECT_ID } = env; 7 | 8 | const redirectUri = "electron://auth/"; // Uses protocol after authenticating 9 | 10 | let accessToken = null; 11 | let refreshToken = null; 12 | let codeVerifier = null; 13 | 14 | const getAuthenticationURL = async (flowParam) => { 15 | codeVerifier = generateCodeVerifier(); 16 | const codeChallenge = await generateCodeChallenge(codeVerifier); 17 | let baseURL = "api.descope.com"; 18 | if (DESCOPE_PROJECT_ID && DESCOPE_PROJECT_ID.length >= 32) { 19 | const localURL = DESCOPE_PROJECT_ID.substring(1, 5); 20 | baseURL = [baseURL.slice(0, 4), localURL, ".", baseURL.slice(4)].join(""); 21 | } 22 | const authUrl = `https://${baseURL}/oauth2/v1/authorize?response_type=code&client_id=${DESCOPE_PROJECT_ID}&redirect_uri=${redirectUri}&scope=openid&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${codeVerifier}&login_hint=${flowParam}`; 23 | return authUrl; 24 | }; 25 | 26 | function generateCodeVerifier() { 27 | let result = ""; 28 | const characters = 29 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; 30 | const charactersLength = characters.length; 31 | 32 | for (let i = 0; i < 128; i++) { 33 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 34 | } 35 | return result; 36 | } 37 | 38 | function generateCodeChallenge(verifier) { 39 | return crypto.subtle 40 | .digest("SHA-256", new TextEncoder().encode(verifier)) 41 | .then((arrayBuffer) => { 42 | const base64Url = btoa( 43 | String.fromCharCode(...new Uint8Array(arrayBuffer)) 44 | ) 45 | .replace(/=/g, "") 46 | .replace(/\+/g, "-") 47 | .replace(/\//g, "_"); 48 | return base64Url; 49 | }); 50 | } 51 | 52 | async function loadTokens(callbackURL) { 53 | const urlParts = url.parse(callbackURL, true); 54 | const query = urlParts.query; 55 | const state = urlParts.state; 56 | 57 | let baseURL = "api.descope.com"; 58 | 59 | const exchangeOptions = { 60 | grant_type: "authorization_code", 61 | client_id: DESCOPE_PROJECT_ID, 62 | redirect_uri: redirectUri, 63 | code: query.code, 64 | code_verifier: codeVerifier, 65 | }; 66 | 67 | const options = { 68 | method: "POST", 69 | url: `https://${baseURL}/oauth2/v1/token`, 70 | headers: { 71 | "content-type": "application/json", 72 | }, 73 | data: JSON.stringify(exchangeOptions), 74 | }; 75 | 76 | try { 77 | const response = await axios(options); 78 | 79 | accessToken = response.data.access_token; 80 | id_token = response.data.id_token; 81 | refreshToken = response.data.refresh_token; 82 | 83 | await storeToken("refresh", refreshToken); 84 | await storeToken("access", accessToken); 85 | } catch (error) { 86 | console.error( 87 | "Error during token exchange:", 88 | error.response.data, 89 | "\n", 90 | error.config 91 | ); 92 | await logout(); 93 | throw error; 94 | } 95 | } 96 | 97 | async function refreshTokens() { 98 | const refreshToken = await retrieveToken("refresh"); 99 | 100 | if (refreshToken) { 101 | const refreshOptions = { 102 | method: "POST", 103 | url: `https://api.descope.com/v1/auth/refresh`, 104 | headers: { 105 | "content-type": "application/json", 106 | Authorization: `Bearer ${DESCOPE_PROJECT_ID}:${refreshToken}`, 107 | }, 108 | data: {}, 109 | }; 110 | 111 | try { 112 | const response = await axios(refreshOptions); 113 | accessToken = response.data.sessionJwt; 114 | await storeToken("access", accessToken); 115 | } catch (error) { 116 | console.error( 117 | "Error during token refresh:", 118 | error.data.response, 119 | error.config 120 | ); 121 | await logout(); 122 | throw error; 123 | } 124 | } else { 125 | console.error("No available refresh token for refreshing session token."); 126 | throw new Error("No available refresh token."); 127 | } 128 | } 129 | 130 | async function validateSession() { 131 | let baseURL = "api.descope.com"; 132 | const exchangeOptions = {}; 133 | 134 | const options = { 135 | method: "POST", 136 | url: `https://${baseURL}/v1/auth/validate`, 137 | headers: { 138 | "content-type": "application/json", 139 | Authorization: `Bearer ${DESCOPE_PROJECT_ID}:${accessToken}`, 140 | }, 141 | data: JSON.stringify(exchangeOptions), 142 | }; 143 | 144 | try { 145 | const response = await axios(options); 146 | 147 | if (response.status === 200) { 148 | return true; 149 | } 150 | } catch (error) { 151 | console.warn("Session validation failed. Attempting to refresh tokens..."); 152 | try { 153 | await refreshTokens(); 154 | return true; 155 | } catch (refreshError) { 156 | console.error( 157 | "Token refresh failed during session validation:", 158 | error.response.data, 159 | error.config 160 | ); 161 | return false; 162 | } 163 | } 164 | 165 | return false; 166 | } 167 | 168 | async function getProfile() { 169 | refreshToken = await retrieveToken("refresh"); 170 | 171 | if (refreshToken) { 172 | const options = { 173 | method: "GET", 174 | url: `https://api.descope.com/v1/auth/me`, 175 | headers: { 176 | Authorization: `Bearer ${DESCOPE_PROJECT_ID}:${refreshToken}`, 177 | }, 178 | }; 179 | 180 | try { 181 | const response = await axios(options); 182 | const name = response.data.name; 183 | const picture = response.data.picture; 184 | const profileInfo = { name: name, picture: picture }; 185 | return profileInfo; 186 | } catch (error) { 187 | console.error( 188 | "Error during get profile axios:", 189 | error.response.data, 190 | error.config 191 | ); 192 | return null; 193 | } 194 | } else { 195 | console.error("No available refresh token in getProfile."); 196 | } 197 | } 198 | 199 | async function logout() { 200 | refreshToken = await retrieveToken("refresh"); 201 | 202 | if (refreshToken) { 203 | let baseURL = "api.descope.com"; 204 | const exchangeOptions = {}; 205 | 206 | const options = { 207 | method: "POST", 208 | url: `https://${baseURL}/v1/auth/logoutall`, 209 | headers: { 210 | "content-type": "application/json", 211 | Authorization: `Bearer ${DESCOPE_PROJECT_ID}:${refreshToken}`, 212 | }, 213 | data: JSON.stringify(exchangeOptions), 214 | }; 215 | 216 | try { 217 | await axios(options); 218 | } catch (error) { 219 | console.error( 220 | "Logout failed, possibly due to invalid or missing refresh token:", 221 | error.response.data, 222 | error.config 223 | ); 224 | } 225 | } 226 | 227 | await storeToken("access", ""); 228 | await storeToken("refresh", ""); 229 | 230 | accessToken = null; 231 | refreshToken = null; 232 | } 233 | 234 | module.exports = { 235 | getAuthenticationURL, 236 | loadTokens, 237 | refreshTokens, 238 | validateSession, 239 | getProfile, 240 | logout, 241 | }; 242 | --------------------------------------------------------------------------------