Skip to content

Commit

Permalink
Merge pull request #1 from drophive/feature/synchronous-support
Browse files Browse the repository at this point in the history
✨ Add synchronous support
  • Loading branch information
dcangulo authored Aug 21, 2020
2 parents 5d977ac + 0284eaf commit dfce592
Show file tree
Hide file tree
Showing 15 changed files with 176 additions and 60 deletions.
6 changes: 6 additions & 0 deletions CHANGELOGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelogs
## 2.0.0
* Added synchronous support.

## 1.0.0
* Initial release.
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,56 @@

Proof Key for Code Exchange (PKCE) challenge generator for React Native.

This module uses [`crypto.randomBytes`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) for web and [`react-native-randombytes`](https://github.com/mvayngrib/react-native-randombytes) for native apps.

## Installation
### Native (iOS / Android)
### Web / iOS / Android
```bash
yarn add react-native-pkce-challenge react-native-randombytes
cd ios; pod install; cd .. # iOS Only
yarn add react-native-pkce-challenge
```

### Web
If you're going to use `asyncPkceChallenge` on **iOS/Android** you also need to do the following.
```bash
yarn add react-native-pkce-challenge
yarn add react-native-randombytes

cd ios; pod install; cd .. # iOS Only
```

## Usage
### Asynchronous (Recommended for iOS/Android)
```js
import pkceChallenge from 'react-native-pkce-challenge'
import {asyncPkceChallenge} from 'react-native-pkce-challenge'

const challenge = await pkceChallenge()
const challenge = await asyncPkceChallenge()
```

It will return:
The asynchronous module uses asynchronous [`crypto.randomBytes`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) for web and [`react-native-randombytes`](https://github.com/mvayngrib/react-native-randombytes) for native apps.

### Synchronous (Not recommended for iOS/Android)
```js
import {pkceChallenge} from 'react-native-pkce-challenge'

const challenge = pkceChallenge()
```

The synchronous module uses synchronous [`crypto.randomBytes`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) for web and [`CryptoJS.lib.WordArray.random`](https://cryptojs.gitbook.io/docs/) for native apps.

The constant `challenge` will hold an object like the following:
```js
{
codeChallenge: 'XsRstqNrXT76Iop3uMoyyCQmaGthJbKKJwXBSoQXaRk',
codeVerifier: 'OZOHUwLddiPyTFJulnUYnU9jsf7oyULflbFpwj40bE9S77iaeisGvzvaVvvPE7oO-xaV4skxwKDFBBV7JofVNxCgUSauqUDVcVjggE4-M6zthVUmeUrSAHatmIBm_P0_'
}
```

## Test
```bash
yarn test
```
### Why asynchronous is recommended for iOS/Android?
[CryptoJS (version 3.3.0)](https://github.com/brix/crypto-js/tree/3.3.0) uses `Math.random()` which gives an output that is not cryptographically secure. [Click this for more info.](https://security.stackexchange.com/questions/181580/why-is-math-random-not-designed-to-be-cryptographically-secure.)

In web this is not a problem since we are using [`crypto.randomBytes`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) which can give us cryptographically secure values.

## Upgrading
See [UPGRADING.md](UPGRADING.md)

## Changelogs
See [CHANGELOGS.md](CHANGELOGS.md)

## References
* https://github.com/crouchcd/pkce-challenge
Expand Down
10 changes: 10 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Upgrading
## From 1.0.0 to 2.0.0
Change:
```js
import pkceChallenge from 'react-native-pkce-challenge'
```
To:
```js
import {asyncPkceChallenge} from 'react-native-pkce-challenge'
```
8 changes: 6 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const pkceChallenge = require('./src/pkce-challenge')
const asyncPkceChallenge = require('./src/async-pkce-challenge')
const pkceChallenge = require('./src/sync-pkce-challenge')

module.exports = pkceChallenge
module.exports = {
asyncPkceChallenge: asyncPkceChallenge,
pkceChallenge: pkceChallenge
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-pkce-challenge",
"version": "1.0.0",
"version": "2.0.0",
"description": "Proof Key for Code Exchange (PKCE) challenge generator for React Native",
"license": "MIT",
"homepage": "https://github.com/drophive/react-native-pkce-challenge#readme",
Expand Down
25 changes: 25 additions & 0 deletions src/async-pkce-challenge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const {base64UrlEncode, generateChallenge} = require('./common')
const generateRandomBytes = require('./async-random-bytes')

function generateVerifier() {
return new Promise(function(resolve, reject) {
generateRandomBytes().then(function(bytes) {
resolve(base64UrlEncode(bytes))
})
})
}

function asyncPkceChallenge() {
return new Promise(function(resolve, reject) {
generateVerifier().then(function(verifier) {
const challenge = generateChallenge(verifier)

resolve({
codeChallenge: challenge,
codeVerifier: verifier
})
})
})
}

module.exports = asyncPkceChallenge
File renamed without changes.
File renamed without changes.
19 changes: 19 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const CryptoJS = require('crypto-js')

function base64UrlEncode(str) {
return str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}

function generateChallenge(verifier) {
const hash = CryptoJS.SHA256(verifier).toString(CryptoJS.enc.Base64)

return base64UrlEncode(hash)
}

module.exports = {
base64UrlEncode: base64UrlEncode,
generateChallenge: generateChallenge
}
38 changes: 0 additions & 38 deletions src/pkce-challenge.js

This file was deleted.

20 changes: 20 additions & 0 deletions src/sync-pkce-challenge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {base64UrlEncode, generateChallenge} = require('./common')
const generateRandomBytes = require('./sync-random-bytes')

function generateVerifier() {
const bytes = generateRandomBytes()

return base64UrlEncode(bytes)
}

function pkceChallenge() {
const verifier = generateVerifier()
const challenge = generateChallenge(verifier)

return {
codeChallenge: challenge,
codeVerifier: verifier
}
}

module.exports = pkceChallenge
10 changes: 10 additions & 0 deletions src/sync-random-bytes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const randomBytes = require('crypto').randomBytes

function generateRandomBytes() {
const buffer = randomBytes(96)
const bytes = buffer.toString('base64')

return bytes
}

module.exports = generateRandomBytes
10 changes: 10 additions & 0 deletions src/sync-random-bytes.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const CryptoJS = require('crypto-js')

function generateRandomBytes() {
const buffer = CryptoJS.lib.WordArray.random(96)
const bytes = buffer.toString(CryptoJS.enc.Base64)

return bytes
}

module.exports = generateRandomBytes
10 changes: 5 additions & 5 deletions tests/index.test.js → tests/async-pkce-challenge.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const {test} = require('tap')
const pkceChallenge = require('../index')
const {asyncPkceChallenge} = require('../index')

test('Verifier length must be 128 characters', function(t) {
pkceChallenge().then(function(challenge) {
asyncPkceChallenge().then(function(challenge) {
t.is(challenge.codeVerifier.length, 128)
t.end()
})
})

test('Challenge length must be 43 characters', function(t) {
pkceChallenge().then(function(challenge) {
asyncPkceChallenge().then(function(challenge) {
t.is(challenge.codeChallenge.length, 43)
t.end()
})
Expand All @@ -18,14 +18,14 @@ test('Challenge length must be 43 characters', function(t) {
test('Verifier must match the pattern', function(t) {
const pattern = /^[A-Za-z\d\-._~]{43,128}$/

pkceChallenge().then(function(challenge) {
asyncPkceChallenge().then(function(challenge) {
t.match(challenge.codeVerifier, pattern)
t.end()
})
})

test('Challenge must not have [=+/]', function(t) {
pkceChallenge().then(function(challenge) {
asyncPkceChallenge().then(function(challenge) {
t.doesNotHave(challenge.codeChallenge, '=')
t.doesNotHave(challenge.codeChallenge, '+')
t.doesNotHave(challenge.codeChallenge, '/')
Expand Down
33 changes: 33 additions & 0 deletions tests/sync-pkce-challenge.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const {test} = require('tap')
const {pkceChallenge} = require('../index')

test('Verifier length must be 128 characters', function(t) {
const challenge = pkceChallenge()

t.is(challenge.codeVerifier.length, 128)
t.end()
})

test('Challenge length must be 43 characters', function(t) {
const challenge = pkceChallenge()

t.is(challenge.codeChallenge.length, 43)
t.end()
})

test('Verifier must match the pattern', function(t) {
const pattern = /^[A-Za-z\d\-._~]{43,128}$/
const challenge = pkceChallenge()

t.match(challenge.codeVerifier, pattern)
t.end()
})

test('Challenge must not have [=+/]', function(t) {
const challenge = pkceChallenge()

t.doesNotHave(challenge.codeChallenge, '=')
t.doesNotHave(challenge.codeChallenge, '+')
t.doesNotHave(challenge.codeChallenge, '/')
t.end()
})

0 comments on commit dfce592

Please sign in to comment.