Skip to content

Instantly share code, notes, and snippets.

@adeperio
Last active January 8, 2022 20:06
Show Gist options
  • Save adeperio/73ce6680d4b80b45e624ab62bacfbdca to your computer and use it in GitHub Desktop.
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
//@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();
}
}
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();
}
});
@uotw
Copy link

uotw commented Jul 2, 2018

Figured it out for anyone who needs help with this: I passed my createWindow and authWindow functions as variables in the authService.requestAccessCode(newUrl,createWindow) function. The in the promise callback I passed the response and authWindow as a variables for the createmainWindow(response, authWindow) function. That gets me back to the main.js file where I can parse the response and call authWindow.close()

@jbreckmckye
Copy link

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