Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PKCE #4

Merged
merged 5 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ section:
externals: {
'node-fetch': 'fetch',
'@sinonjs/text-encoding': 'TextEncoder',
'whatwg-url': 'window',
'isomorphic-webcrypto': 'crypto'
}
```
Expand Down
41 changes: 29 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,42 @@
},
"homepage": "https://github.com/solid/oidc-rp",
"dependencies": {
"@solid/jose": "^0.5.0",
"base64url": "^3.0.1",
"isomorphic-webcrypto": "^2.3.2",
"node-fetch": "^2.6.0",
"@solid/jose": "solid/jose#browser",
"assert": "^2.0.0",
"base64url-universal": "^1.1.0",
"bnid": "^2.0.0",
"isomorphic-fetch": "^3.0.0",
"isomorphic-webcrypto": "^2.3.6",
"standard-http-error": "^2.0.1",
"whatwg-url": "^7.1.0"
"universal-base64": "^2.1.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"dirty-chai": "^2.0.1",
"mocha": "^6.2.2",
"nock": "^11.7.0",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"standard": "^14.3.1",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10"
"mocha": "^8.2.1",
"nock": "^13.0.5",
"sinon": "^9.2.3",
"sinon-chai": "^3.5.0",
"standard": "^16.0.3",
"webpack": "^5.13.0",
"webpack-cli": "^4.3.1"
},
"browser": {
"buffer": false,
"crypto": false
},
"standard": {
"globals": [
"after",
"afterEach",
"before",
"beforeEach",
"describe",
"fetch",
"it"
]
}
}
28 changes: 22 additions & 6 deletions src/AuthenticationRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* Dependencies
*/
const assert = require('assert')
const base64url = require('base64url')
const crypto = require('isomorphic-webcrypto')
const { encode: base64urlEncode } = require('base64url-universal')
const { JWT } = require('@solid/jose')
const FormUrlEncoded = require('./FormUrlEncoded')
const { URL } = require('whatwg-url')
const { IdGenerator } = require('bnid')

/**
* Authentication Request
Expand All @@ -22,8 +22,8 @@ class AuthenticationRequest {
* request URI.
*
* @param {RelyingParty} rp – instance of RelyingParty
* @param {Object} options - optional request parameters
* @param {Object} session – reference to localStorage or other session object
* @param {object} options - optional request parameters
* @param {object} session – reference to localStorage or other session object
*
* @returns {Promise}
*/
Expand Down Expand Up @@ -67,6 +67,13 @@ class AuthenticationRequest {
assert(params.redirect_uri,
'Missing redirect_uri parameter in authentication request')

// generate code_verifier random octets for PKCE
// @see https://tools.ietf.org/html/rfc7636
const generator = new IdGenerator({ bitLength: 256 })
const codeVerifier = base64urlEncode(await generator.generate())
// store verifier, for future use with token endpoint
params.code_verifier = codeVerifier

// generate state and nonce random octets
params.state = Array.from(crypto.getRandomValues(new Uint8Array(16)))
params.nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)))
Expand All @@ -80,8 +87,8 @@ class AuthenticationRequest {
// serialize the request with original values, store in session by
// encoded state param, and replace state/nonce octets with encoded
// digests
const state = base64url(Buffer.from(digests[0]))
const nonce = base64url(Buffer.from(digests[1]))
const state = base64urlEncode(digests[0])
const nonce = base64urlEncode(digests[1])
const key = `${issuer}/requestHistory/${state}`

// store the request params for response validation
Expand All @@ -92,6 +99,15 @@ class AuthenticationRequest {
params.state = state
params.nonce = nonce

// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
const encoder = new TextEncoder()
params.code_challenge = base64urlEncode(
await crypto.subtle.digest(
{ name: 'SHA-256' }, encoder.encode(codeVerifier)
)
)
params.code_challenge_method = 'S256'

const sessionKeys = await AuthenticationRequest.generateSessionKeys()
await AuthenticationRequest.storeSessionKeys(sessionKeys, params, session)

Expand Down
32 changes: 16 additions & 16 deletions src/AuthenticationResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
/**
* Dependencies
*/
const { URL } = require('whatwg-url')
require('isomorphic-fetch')
const assert = require('assert')
const crypto = require('isomorphic-webcrypto')
const base64url = require('base64url')
const fetch = require('node-fetch')
const { encode: base64urlEncode } = require('base64url-universal')
const Headers = fetch.Headers ? fetch.Headers : global.Headers
const FormUrlEncoded = require('./FormUrlEncoded')
const IDToken = require('./IDToken')
const Session = require('./Session')
const onHttpError = require('./onHttpError')
const HttpError = require('standard-http-error')
const { encode: base64encode } = require('universal-base64')

/**
* AuthenticationResponse
Expand Down Expand Up @@ -182,7 +182,7 @@ class AuthenticationResponse {
const encoded = response.params.state

const digest = await crypto.subtle.digest({ name: 'SHA-256' }, octets)
if (encoded !== base64url(Buffer.from(digest))) {
if (encoded !== base64urlEncode(digest)) {
throw new Error(
'Mismatching state parameter in authentication response.')
}
Expand All @@ -193,7 +193,7 @@ class AuthenticationResponse {
/**
* validateResponseMode
*
* @param {Object} response
* @param {object} response
*/
static validateResponseMode (response) {
if (response.request.response_type !== 'code' && response.mode === 'query') {
Expand Down Expand Up @@ -268,6 +268,7 @@ class AuthenticationResponse {
const bodyContents = {
grant_type: 'authorization_code',
code: code,
code_verifier: response.request.code_verifier,
redirect_uri: request.redirect_uri
}

Expand All @@ -277,8 +278,7 @@ class AuthenticationResponse {

// client secret basic authentication
if (authMethod === 'client_secret_basic') {
const credentials = Buffer.from(`${id}:${secret}`)
.toString('base64')
const credentials = base64encode(`${id}:${secret}`)
headers.set('Authorization', `Basic ${credentials}`)
}

Expand Down Expand Up @@ -344,7 +344,7 @@ class AuthenticationResponse {
/**
* decryptIDToken
*
* @param {Object} response
* @param {object} response
* @returns {Promise}
*/
static async decryptIDToken (response) {
Expand Down Expand Up @@ -399,7 +399,7 @@ class AuthenticationResponse {
/**
* validateAudience
*
* @param {Object} response
* @param {object} response
*/
static validateAudience (response) {
const registration = response.rp.registration
Expand Down Expand Up @@ -498,7 +498,7 @@ class AuthenticationResponse {
/**
* verifyNonce
*
* @param {Object} response
* @param {object} response
* @returns {Promise}
*/
static async verifyNonce (response) {
Expand All @@ -510,16 +510,16 @@ class AuthenticationResponse {
}

const digest = await crypto.subtle.digest({ name: 'SHA-256' }, octets)
if (nonce !== base64url(Buffer.from(digest))) {
if (nonce !== base64urlEncode(digest)) {
throw new Error('Mismatching nonce in ID Token.')
}
}

/**
* validateAcr
*
* @param {Object} response
* @returns {Object}
* @param {object} response
* @returns {object}
*/
static validateAcr (response) {
// TODO
Expand All @@ -529,7 +529,7 @@ class AuthenticationResponse {
/**
* validateAuthTime
*
* @param {Object} response
* @param {object} response
* @returns {Promise}
*/
static validateAuthTime (response) {
Expand All @@ -540,7 +540,7 @@ class AuthenticationResponse {
/**
* validateAccessTokenHash
*
* @param {Object} response
* @param {object} response
* @returns {Promise}
*/
static validateAccessTokenHash (response) {
Expand All @@ -551,7 +551,7 @@ class AuthenticationResponse {
/**
* validateAuthorizationCodeHash
*
* @param {Object} response
* @param {object} response
* @returns {Promise}
*/
static validateAuthorizationCodeHash (response) {
Expand Down
19 changes: 9 additions & 10 deletions src/FormUrlEncoded.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* FormUrlEncoded
*/
class FormUrlEncoded {

/**
* Encode
*
Expand All @@ -17,13 +16,13 @@ class FormUrlEncoded {
* @returns {string}
*/
static encode (data) {
let pairs = []
const pairs = []

Object.keys(data).forEach(function (key) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
})
Object.keys(data).forEach(function (key) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
})

return pairs.join('&')
return pairs.join('&')
}

/**
Expand All @@ -36,12 +35,12 @@ class FormUrlEncoded {
* @returns {Object}
*/
static decode (data) {
let obj = {}
const obj = {}

data.split('&').forEach(function (property) {
let pair = property.split('=')
let key = decodeURIComponent(pair[0])
let val = decodeURIComponent(pair[1])
const pair = property.split('=')
const key = decodeURIComponent(pair[0])
const val = decodeURIComponent(pair[1])

obj[key] = val
})
Expand Down
6 changes: 4 additions & 2 deletions src/IDToken.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable camelcase */
'use strict'
/**
* Local dependencies
*/
const {JWT} = require('@solid/jose')
const { JWT } = require('@solid/jose')

const REQUIRED_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat']

Expand Down Expand Up @@ -69,7 +71,7 @@ class IDToken extends JWT {
return payloadResult
}

let valid = true
const valid = true
let error

return { valid, error }
Expand Down
1 change: 0 additions & 1 deletion src/PoPToken.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const { URL } = require('whatwg-url')
const { JWT, JWK } = require('@solid/jose')

const DEFAULT_MAX_AGE = 3600 // Default token expiration, in seconds
Expand Down
4 changes: 2 additions & 2 deletions src/RelyingParty.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
'use strict'
/**
* Dependencies
*/
require('isomorphic-fetch')
const assert = require('assert')
const fetch = require('node-fetch')
const { URL } = require('whatwg-url')
const Headers = fetch.Headers ? fetch.Headers : global.Headers
const { JWKSet } = require('@solid/jose')
const AuthenticationRequest = require('./AuthenticationRequest')
Expand Down
6 changes: 4 additions & 2 deletions src/Session.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const fetch = require('node-fetch')
require('isomorphic-fetch')
const onHttpError = require('./onHttpError')
const PoPToken = require('./PoPToken')

Expand Down Expand Up @@ -58,7 +58,9 @@ class Session {
const rpAuthOptions = rp.defaults.authenticate || {}

const credentialType = rpAuthOptions.credential_type ||
rp.defaults.popToken ? 'pop_token' : 'access_token'
rp.defaults.popToken
? 'pop_token'
: 'access_token'

const sessionKey = response.session[RelyingParty.SESSION_PRIVATE_KEY]

Expand Down
4 changes: 2 additions & 2 deletions src/onHttpError.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ function onHttpError (message = 'fetch error') {
return response
}

let errorMessage = `${message}: ${response.status} ${response.statusText}`
let error = new Error(errorMessage)
const errorMessage = `${message}: ${response.status} ${response.statusText}`
const error = new Error(errorMessage)
error.response = response
error.statusCode = response.status
throw error
Expand Down
Loading