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 1, 2018

I've added my website as the api audience, not sure if I'll need to configure it on auth0 or not. One other note - launching electron still gives the same console error, and no authorize modal is presented, but the browser looks like it is being immediately redirected to the https://myapp.auth0.com/mobile link, I get an "OK" response in the window.

@uotw
Copy link

uotw commented Jul 2, 2018

OK, I've got it working for my use case. Because I'm not implementing a custom API, and just need access to the user's info, I changed the config info as follows:

  var authConfig : AuthServiceConfig = {
    clientId: '...',
    authorizeEndpoint: 'https://ultrasoundjelly.auth0.com/authorize',
    audience: 'https://ultrasoundjelly.auth0.com/userinfo',
    scope: 'openid',
    redirectUri: 'https://ultrasoundjelly.auth0.com/mobile',
    tokenEndpoint: 'https://ultrasoundjelly.auth0.com/oauth/token'
  }

Now I'm getting the auth modal, successfully log in, and get a valid id_token echoed to the console to send to parse for user info. I am throwing a warning three times with this code, however:

Access code callback url not expected.

Aside from correcting this error. Now I'm trying to figure out how to 1) capture redirect event 2) show index.html 3) send my token to renderer.js. This code works for capturing the redirect:

  authWindow.webContents.on('will-navigate', function(){
      authWindow.close();
      createmainWindow();   //this function creates my main app window and navigates to my index.html
  });

No luck getting the token out of the promise. I've tried using localStorage.setItem and setting a global variable without luck. Any thoughts on returning the value to main.js so I can deal with it from there?

@uotw
Copy link

uotw commented Jul 2, 2018

Actually, nix that last code. If I call authWindow.close() on will-navigate I lose the promise response. I need to figure out how to hook into the promise from main.js.

@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