Repository: auth0/node-auth0
Pull request #597 (2021-03-23 00:09:33)

Org idtoken validation support
Adds support for validating organization when using `authorizationCodeGrant`


Patch:
diff --git a/src/auth/OAUthWithIDTokenValidation.js b/src/auth/OAUthWithIDTokenValidation.js
index bac2b321b..3561a9382 100644
--- a/src/auth/OAUthWithIDTokenValidation.js
+++ b/src/auth/OAUthWithIDTokenValidation.js
@@ -86,6 +86,10 @@ OAUthWithIDTokenValidation.prototype.create = function(params, data, cb) {
           issuer: 'https://' + this.domain + '/'
         };
 
+        if (data.organization) {
+          options.organization = data.organization;
+        }
+
         if (data.nonce) {
           options.nonce = data.nonce;
         }
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index 22a58a475..d34a55c63 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -359,6 +359,7 @@ OAuthAuthenticator.prototype.clientCredentialsGrant = function(options, cb) {
  *   redirect_uri: '{REDIRECT_URI}',
  *   client_id: '{CLIENT_ID}',  // Optional field.
  *   client_secret: '{CLIENT_SECRET}',  // Optional field.
+ *   organization: '{ORGANIZATION_ID}' // Optiional field.
  * };
  *
  * auth0.oauth.authorizationCodeGrant(data, function (err, userData) {
@@ -370,6 +371,7 @@ OAuthAuthenticator.prototype.clientCredentialsGrant = function(options, cb) {
  * });
  *
  * @param   {Object}    data                  Authorization code payload
+ * @param   {String}    userData.organization Organization ID
  * @param   {String}    userData.code         Code in URL returned after authentication
  * @param   {String}    userData.redirect_uri The URL to which Auth0 will redirect the browser after authorization has been granted by the user.
  *
diff --git a/src/auth/idToken.js b/src/auth/idToken.js
index 10bf5302a..353117c09 100644
--- a/src/auth/idToken.js
+++ b/src/auth/idToken.js
@@ -79,6 +79,19 @@ var validate = function(token, options) {
     );
   }
 
+  // Organization
+  if (options.organization) {
+    if (!payload.org_id) {
+      throw new Error('Organization Id (org_id) claim must be a string present in the ID token');
+    }
+
+    if (payload.org_id !== options.organization) {
+      throw new Error(
+        `Organization Id (org_id) claim value mismatch in the ID token; expected "${options.organization}", found "${payload.org_id}"'`
+      );
+    }
+  }
+
   // --Time validation (epoch)--
   var now = Math.floor(Date.now() / 1000);
   var leeway = options.leeway || DEFAULT_LEEWAY;
diff --git a/src/auth/index.js b/src/auth/index.js
index d2afb1c1a..77cdaf671 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -41,6 +41,7 @@ var BASE_URL_FORMAT = 'https://%s';
  * @param   {String}  options.domain                    AuthenticationClient server domain.
  * @param   {String}  [options.clientId]                Default client ID.
  * @param   {String}  [options.clientSecret]            Default client Secret.
+ * @param   {String}  [options.organization]            Organization ID.
  * @param   {String}  [options.supportedAlgorithms]     Algorithms that your application expects to receive
  * @param  {Boolean}  [options.__bypassIdTokenValidation] Whether the id_token should be validated or not
  * @param   {Object}  [options.headers]                 Additional headers that will be added to the outgoing requests.


Test patch:
diff --git a/test/idToken.tests.js b/test/idToken.tests.js
index 6a99ad571..e427e4b2f 100644
--- a/test/idToken.tests.js
+++ b/test/idToken.tests.js
@@ -63,6 +63,7 @@ var payload = {
   iat: yesterday(),
   nonce: expectations.nonce,
   azp: expectations.clientId,
+  org_id: 'test|org',
   auth_time: TODAY_IN_SECONDS
 };
 
@@ -258,4 +259,27 @@ describe('idToken.validate', function() {
       'Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication.'
     );
   });
+  it('should throw when organization is in options, but org_id missing from claim', function() {
+    expectedOptions.organization = 'testorg';
+
+    expect(function() {
+      idToken.validate(generateJWT({ org_id: undefined }), expectedOptions);
+    }).to.throw('Organization Id (org_id) claim must be a string present in the ID token');
+  });
+  it('should throw when org claim doesnt match org expected', function() {
+    expectedOptions.organization = 'testorg';
+
+    expect(function() {
+      idToken.validate(generateJWT({ org_id: 'notExpectedOrg' }), expectedOptions);
+    }).to.throw(
+      'Organization Id (org_id) claim value mismatch in the ID token; expected "testorg", found "notExpectedOrg'
+    );
+  });
+  it('should NOT throw when org_id matches expected organization', function() {
+    expectedOptions.organization = 'testorg';
+
+    expect(function() {
+      idToken.validate(generateJWT({ org_id: 'testorg' }), expectedOptions);
+    }).not.to.throw();
+  });
 });