-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Implemented auth0 adapater. #7502
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
base: alpha
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,7 @@ describe('AuthenticationProviders', function () { | |
'phantauth', | ||
'microsoft', | ||
'keycloak', | ||
'auth0', | ||
].map(function (providerName) { | ||
it('Should validate structure of ' + providerName, done => { | ||
const provider = require('../lib/Adapters/Auth/' + providerName); | ||
|
@@ -62,7 +63,7 @@ describe('AuthenticationProviders', function () { | |
}); | ||
|
||
it(`should provide the right responses for adapter ${providerName}`, async () => { | ||
const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak']; | ||
const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak', 'auth0']; | ||
if (noResponse.includes(providerName)) { | ||
return; | ||
} | ||
|
@@ -1024,6 +1025,183 @@ describe('keycloak auth adapter', () => { | |
}); | ||
}); | ||
|
||
describe('auth0 auth adapter', () => { | ||
const auth0 = require('../lib/Adapters/Auth/auth0'); | ||
const jwt = require('jsonwebtoken'); | ||
const util = require('util'); | ||
|
||
it('Should throw error with missing id_token', async () => { | ||
try { | ||
await auth0.validateAuthData({}, { tenantId: 'example.eu.auth0.com', clientId: 'secret' }); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('id token is invalid for this user.'); | ||
} | ||
}); | ||
|
||
it('Should throw error with missing tenant_id', async () => { | ||
try { | ||
await auth0.validateAuthData({ id_token: 'token' }, { clientId: 'secret' }); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('tenant id is invalid.'); | ||
} | ||
}); | ||
|
||
it('Should throw error with missing client_id', async () => { | ||
try { | ||
await auth0.validateAuthData({ id_token: 'token' }, { tenantId: 'example.eu.auth0.com' }); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('client id is invalid.'); | ||
} | ||
}); | ||
|
||
it('should not decode invalid id_token', async () => { | ||
try { | ||
await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('provided token does not decode as JWT'); | ||
} | ||
}); | ||
|
||
it('should throw error if public key used to encode token is not available', async () => { | ||
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } }; | ||
try { | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
|
||
await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe( | ||
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid} for auth0 tenantId example.eu.auth0.com.` | ||
); | ||
} | ||
}); | ||
|
||
it('should use algorithm from key header to verify id_token', async () => { | ||
const fakeClaim = { | ||
iss: 'https://example.eu.auth0.com/', | ||
aud: 'secret', | ||
exp: Date.now(), | ||
sub: 'the_user_id', | ||
}; | ||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim); | ||
const fakeGetSigningKeyAsyncFunction = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could probably simplify this to: const fakeGetSigningKeyAsyncFunction = () => ({ kid: '123', rsaPublicKey: 'the_rsa_public_key' }) |
||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; | ||
}; | ||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction); | ||
|
||
const result = await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
expect(result).toEqual(fakeClaim); | ||
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg); | ||
}); | ||
|
||
it('should not verify invalid id_token', async () => { | ||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
const fakeGetSigningKeyAsyncFunction = () => { | ||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; | ||
}; | ||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction); | ||
|
||
try { | ||
await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a URI that is made available for development use per policy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not, as far as I know, I just choose a value for the tests. Since the tenant is not really used in the tests, it does not really matter. Another option would be to create an auth0 account and register a tenant, but then someone from the Parse team would have to manage this account. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any benefit regarding test efficacy if we create an auth0 account? If not, I suggest we comply with RFC 2606 and replace the URLs in the tests with |
||
); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('jwt malformed'); | ||
} | ||
}); | ||
|
||
it('should verify id_token', async () => { | ||
const fakeClaim = { | ||
iss: 'https://example.eu.auth0.com/', | ||
aud: 'secret', | ||
exp: Date.now(), | ||
sub: 'the_user_id', | ||
}; | ||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
const fakeGetSigningKeyAsyncFunction = () => { | ||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; | ||
}; | ||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction); | ||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim); | ||
|
||
const result = await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
expect(result).toEqual(fakeClaim); | ||
}); | ||
|
||
it('should throw error with with invalid jwt issuer', async () => { | ||
const fakeClaim = { | ||
iss: 'https://not.example.eu.auth0.com/', | ||
sub: 'the_user_id', | ||
}; | ||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
const fakeGetSigningKeyAsyncFunction = () => { | ||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; | ||
}; | ||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction); | ||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim); | ||
|
||
try { | ||
await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe( | ||
'id token not issued by correct OpenID provider - expected: https://example.eu.auth0.com/ | from: https://not.example.eu.auth0.com/' | ||
); | ||
} | ||
}); | ||
|
||
it('should throw error with with invalid user id', async () => { | ||
const fakeClaim = { | ||
iss: 'https://example.eu.auth0.com/', | ||
aud: 'invalid_client_id', | ||
sub: 'a_different_user_id', | ||
}; | ||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; | ||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); | ||
const fakeGetSigningKeyAsyncFunction = () => { | ||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; | ||
}; | ||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction); | ||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim); | ||
|
||
try { | ||
await auth0.validateAuthData( | ||
{ id: 'the_user_id', id_token: 'the_token' }, | ||
{ tenantId: 'example.eu.auth0.com', clientId: 'secret' } | ||
); | ||
fail(); | ||
} catch (e) { | ||
expect(e.message).toBe('auth data is invalid for this user.'); | ||
} | ||
}); | ||
}); | ||
|
||
describe('oauth2 auth adapter', () => { | ||
const oauth2 = require('../lib/Adapters/Auth/oauth2'); | ||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
'use strict'; | ||
|
||
// tenantId: the auth0 tenant, e.g. example.eu.auth0.com (NO HTTP / HTTPS) | ||
// clientId: the auth0 client id, can be found in the auth0 console. | ||
|
||
var Parse = require('parse/node').Parse; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no var |
||
const jwksClient = require('jwks-rsa'); | ||
const jwt = require('jsonwebtoken'); | ||
const util = require('util'); | ||
|
||
const getAuth0KeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge, tenantId) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some functions are arrow syntax others are regular function syntax - I would recommend consistency |
||
const client = jwksClient({ | ||
jwksUri: `https://${tenantId}/.well-known/jwks.json`, | ||
cache: true, | ||
cacheMaxEntries, | ||
cacheMaxAge, | ||
}); | ||
|
||
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey); | ||
|
||
let key; | ||
try { | ||
key = await asyncGetSigningKeyFunction(keyId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer |
||
} catch (error) { | ||
throw new Parse.Error( | ||
Parse.Error.OBJECT_NOT_FOUND, | ||
`Unable to find matching key for Key ID: ${keyId} for auth0 tenantId ${tenantId}.` | ||
); | ||
} | ||
return key; | ||
}; | ||
|
||
function getHeaderFromToken(token) { | ||
const decodedToken = jwt.decode(token, { complete: true }); | ||
|
||
if (!decodedToken) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`); | ||
} | ||
|
||
return decodedToken.header; | ||
} | ||
|
||
async function verifyIdToken( | ||
{ id_token: token, id }, | ||
{ tenantId, clientId, cacheMaxEntries, cacheMaxAge } | ||
) { | ||
if (!token) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`); | ||
} | ||
if (!clientId) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `client id is invalid.`); | ||
} | ||
if (!tenantId) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `tenant id is invalid.`); | ||
} | ||
|
||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token); | ||
let jwtClaims; | ||
const ONE_HOUR_IN_MS = 3600000; | ||
cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use default parameters on the object destructure? |
||
cacheMaxEntries = cacheMaxEntries || 5; | ||
|
||
const auth0Key = await getAuth0KeyByKeyId(keyId, cacheMaxEntries, cacheMaxAge, tenantId); | ||
const signingKey = auth0Key.publicKey || auth0Key.rsaPublicKey; | ||
|
||
try { | ||
jwtClaims = jwt.verify(token, signingKey, { | ||
algorithms: algorithm, | ||
// the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. | ||
audience: clientId, | ||
}); | ||
} catch (exception) { | ||
const message = exception.message; | ||
|
||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`); | ||
} | ||
|
||
const tokenIssuer = `https://${tenantId}/`; | ||
|
||
if (jwtClaims.iss !== tokenIssuer) { | ||
throw new Parse.Error( | ||
Parse.Error.OBJECT_NOT_FOUND, | ||
`id token not issued by correct OpenID provider - expected: ${tokenIssuer} | from: ${jwtClaims.iss}` | ||
); | ||
} | ||
|
||
if (jwtClaims.sub !== id) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`); | ||
} | ||
return jwtClaims; | ||
} | ||
|
||
// Returns a promise that fulfills if this user id is valid. | ||
function validateAuthData(authData, options = {}) { | ||
return verifyIdToken(authData, options); | ||
} | ||
|
||
// Returns a promise that fulfills if this app id is valid. | ||
function validateAppId() { | ||
return Promise.resolve(); | ||
} | ||
|
||
module.exports = { | ||
validateAppId: validateAppId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can use destructuring assignment here, i.e:
|
||
validateAuthData: validateAuthData, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer