Last active
January 8, 2022 20:06
-
-
Save adeperio/73ce6680d4b80b45e624ab62bacfbdca to your computer and use it in GitHub Desktop.
PKCE flow in Electron with Passwordless. In ES6 + flow + request-promise. Executes PKCE through the Electron BrowserWindow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//@flow | |
import request from 'request' | |
import crypto from 'crypto' | |
import rp from 'request-promise' | |
export type AuthServiceConfig = { | |
authorizeEndpoint: string, | |
clientId: string, | |
audience: string, | |
scope: string, | |
redirectUri: string, | |
tokenEndpoint: string | |
} | |
//https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce | |
export default class AuthService { | |
challengePair : { verifier: string, challenge: string } | |
config: AuthServiceConfig | |
constructor(config: AuthServiceConfig){ | |
this.config = config | |
} | |
requestAuthCode() : string { | |
this.challengePair = AuthService.getPKCEChallengePair() | |
return this.getAuthoriseUrl(this.challengePair) | |
} | |
requestAccessCode(callbackUrl: string): Promise<any> { | |
return new Promise((resolve, reject) => { | |
if(this.isValidAccessCodeCallBackUrl(callbackUrl)) { | |
let authCode = AuthService.getParameterByName('code', callbackUrl) | |
if(authCode != null){ | |
let verifier = this.challengePair.verifier | |
let options = this.getTokenPostRequest(authCode, verifier) | |
return rp(options) | |
.then(function(response) { | |
//TODO: return / store access code, | |
//remove console.log, meant for demonstration purposes only | |
console.log('access token.response: ' + JSON.stringify(response)); | |
}) | |
.catch(function (err) { | |
if (err) throw new Error(err); | |
}); | |
} else { | |
reject('Could not parse the authorization code') | |
} | |
} else { | |
reject('Access code callback url not expected.') | |
} | |
}) | |
} | |
getAuthoriseUrl(challengePair: { verifier: string, challenge: string }) : string { | |
return `${this.config.authorizeEndpoint}?audience=${this.config.audience}&scope=${this.config.scope}&response_type=code&client_id=${this.config.clientId}&code_challenge=${challengePair.challenge}&code_challenge_method=S256&redirect_uri=${this.config.redirectUri}` | |
} | |
getTokenPostRequest(authCode: string, verifier: string){ | |
return { | |
method: 'POST', | |
url: this.config.tokenEndpoint, | |
headers: { 'content-type': 'application/json' }, | |
body: `{"grant_type":"authorization_code", | |
"client_id": "${this.config.clientId}", | |
"code_verifier": "${verifier}", | |
"code": "${authCode}", | |
"redirect_uri":"${this.config.redirectUri}" | |
}` | |
}; | |
} | |
isValidAccessCodeCallBackUrl(callbackUrl: string) : boolean { | |
return callbackUrl.indexOf(this.config.redirectUri) > -1 | |
} | |
static getPKCEChallengePair() : { verifier: string, challenge: string } { | |
let verifier = AuthService.base64URLEncode(crypto.randomBytes(32)); | |
let challenge = AuthService.base64URLEncode(AuthService.sha256(verifier)); | |
return { verifier, challenge }; | |
} | |
static getParameterByName(name: string, url: string) : ?string { | |
name = name.replace(/[\[\]]/g, "\\$&"); | |
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), | |
results = regex.exec(url); | |
if (!results) return null; | |
if (!results[2]) return ''; | |
return decodeURIComponent(results[2].replace(/\+/g, " ")); | |
} | |
static base64URLEncode(str: Buffer) : string { | |
return str.toString('base64') | |
.replace(/\+/g, '-') | |
.replace(/\//g, '_') | |
.replace(/=/g, ''); | |
} | |
static sha256(buffer: string) : Buffer { | |
return crypto.createHash('sha256').update(buffer).digest(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import path from "path"; | |
import events from 'events' | |
import { app, BrowserWindow } from "electron"; | |
import AuthService, { AuthServiceConfig } from "./AuthService" | |
function getAuthConfig(){ | |
//sample values - plug your Auth0 config here | |
var authConfig : AuthServiceConfig = { | |
clientId: 'rlasjf82130948asdkfjaslsaklaskfd', | |
authorizeEndpoint: 'https://myapp.auth0.com/authorize', | |
audience: 'https://myapi.com:8080', | |
scope: 'email%20given_name%20profile', | |
redirectUri: 'https://myapp.auth0.com/mobile', | |
tokenEndpoint: 'https://myapp.auth0.com/oauth/token' | |
} | |
return authConfig | |
} | |
app.on("ready", () => { | |
let authService = new AuthService(getAuthConfig()) | |
let authWindow = new BrowserWindow({ width: 800, height: 600 }) | |
/* | |
Go to hosted login page at the authorise endpoint | |
authenticate | |
and request auth code, and send challenge | |
*/ | |
authWindow.loadURL(authService.requestAuthCode()); | |
authWindow.webContents.on('did-get-redirect-request', function(event, oldUrl, newUrl) { | |
/* | |
after successfuly authenticating | |
get auth code from the redirect uri | |
and use that and the code verifier | |
to request an access code | |
*/ | |
authService.requestAccessCode(newUrl) | |
}); | |
}); | |
app.on('window-all-closed', () => { | |
// Respect the OSX convention of having the application in memory even | |
// after all windows have been closed | |
if (process.platform !== 'darwin') { | |
app.quit(); | |
} | |
}); | |
I've created a package that should handle both this and refresh token persistence. Check it out: https://github.com/jbreckmckye/electron-auth0-login
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Figured it out for anyone who needs help with this: I passed my
createWindow
andauthWindow
functions as variables in theauthService.requestAccessCode(newUrl,createWindow)
function. The in the promise callback I passed the response andauthWindow
as a variables for thecreatemainWindow(response, authWindow)
function. That gets me back to the main.js file where I can parse the response and callauthWindow.close()