From 897c825bff64393d67075d56c85e09d72b30f3c5 Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Thu, 13 Jul 2023 10:44:50 +0100 Subject: [PATCH 1/5] chore: update Ruby version in devcontainer --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a29618e..77db143 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Ruby", - "image": "mcr.microsoft.com/devcontainers/ruby:3.1", + "image": "mcr.microsoft.com/devcontainers/ruby:3.2", "features": { "ghcr.io/devcontainers/features/node:1": { "version": "lts" From d15af31f1292a5b471558c7fe0f88711e7bffa0a Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Thu, 13 Jul 2023 12:10:40 +0000 Subject: [PATCH 2/5] feat: support organization name in JWT validation --- lib/omniauth/auth0/jwt_validator.rb | 22 +++- spec/omniauth/auth0/jwt_validator_spec.rb | 122 ++++++++++++++++------ 2 files changed, 110 insertions(+), 34 deletions(-) diff --git a/lib/omniauth/auth0/jwt_validator.rb b/lib/omniauth/auth0/jwt_validator.rb index 4484734..640e5a5 100644 --- a/lib/omniauth/auth0/jwt_validator.rb +++ b/lib/omniauth/auth0/jwt_validator.rb @@ -7,6 +7,7 @@ module OmniAuth module Auth0 # JWT Validator class + # rubocop:disable Metrics/ class JWTValidator attr_accessor :issuer, :domain @@ -264,12 +265,27 @@ def verify_auth_time(id_token, leeway, max_age) end def verify_org(id_token, organization) - if organization + return unless organization + + validate_as_id = organization.start_with? 'org_' + + if validate_as_id org_id = id_token['org_id'] if !org_id || !org_id.is_a?(String) - raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim must be a string present in the ID token") + raise OmniAuth::Auth0::TokenValidationError, + 'Organization Id (org_id) claim must be a string present in the ID token' elsif org_id != organization - raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'") + raise OmniAuth::Auth0::TokenValidationError, + "Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'" + end + else + org_name = id_token['org_name'] + if !org_name || !org_name.is_a?(String) + raise OmniAuth::Auth0::TokenValidationError, + 'Organization Name (org_name) claim must be a string present in the ID token' + elsif org_name.downcase != organization.downcase + raise OmniAuth::Auth0::TokenValidationError, + "Organization Name (org_name) claim value mismatch in the ID token; expected '#{organization}', found '#{org_name}'" end end end diff --git a/spec/omniauth/auth0/jwt_validator_spec.rb b/spec/omniauth/auth0/jwt_validator_spec.rb index b9ad5f4..7fe6273 100644 --- a/spec/omniauth/auth0/jwt_validator_spec.rb +++ b/spec/omniauth/auth0/jwt_validator_spec.rb @@ -476,41 +476,101 @@ expect(id_token['auth_time']).to eq(auth_time) end - it 'should fail when authorize params has organization but org_id is missing in the token' do - payload = { - iss: "https://#{domain}/", - sub: 'sub', - aud: client_id, - exp: future_timecode, - iat: past_timecode - } + context 'Organization claim validation', focus: true do + it 'should fail when authorize params has organization but org_id is missing in the token' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode + } - token = make_hs256_token(payload) - expect do - jwt_validator.verify(token, { organization: 'Test Org' }) - end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ - message: "Organization Id (org_id) claim must be a string present in the ID token" - })) - end + token = make_hs256_token(payload) + expect do + jwt_validator.verify(token, { organization: 'org_123' }) + end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ + message: "Organization Id (org_id) claim must be a string present in the ID token" + })) + end - it 'should fail when authorize params has organization but token org_id does not match' do - payload = { - iss: "https://#{domain}/", - sub: 'sub', - aud: client_id, - exp: future_timecode, - iat: past_timecode, - org_id: 'Wrong Org' - } + it 'should fail when authorize params has organization but org_name is missing in the token' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode + } - token = make_hs256_token(payload) - expect do - jwt_validator.verify(token, { organization: 'Test Org' }) - end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ - message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'Test Org', found 'Wrong Org'" - })) - end + token = make_hs256_token(payload) + expect do + jwt_validator.verify(token, { organization: 'my-organization' }) + end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({ + message: 'Organization Name (org_name) claim must be a string present in the ID token' + }))) + end + + it 'should fail when authorize params has organization but token org_id does not match' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_id: 'org_5678' + } + + token = make_hs256_token(payload) + expect do + jwt_validator.verify(token, { organization: 'org_1234' }) + end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({ + message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'org_1234', found 'org_5678'" + }))) + end + + it 'should not fail when correctly given an organization ID' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_id: 'org_1234' + } + + token = make_hs256_token(payload) + jwt_validator.verify(token, { organization: 'org_1234' }) + end + + it 'should not fail when correctly given an organization name' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_name: 'my-organization' + } + + token = make_hs256_token(payload) + jwt_validator.verify(token, { organization: 'my-organization' }) + end + it 'should not fail when given an organization name in a different casing' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_name: 'MY-ORGANIZATION' + } + + token = make_hs256_token(payload) + jwt_validator.verify(token, { organization: 'my-organization' }) + end + end it 'should fail for RS256 token when kid is incorrect' do domain = 'example.org' sub = 'abc123' From ee69a562531692142dde54f039159a92d7bc042b Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Thu, 13 Jul 2023 12:16:38 +0000 Subject: [PATCH 3/5] chore: fixed up the docs example --- EXAMPLES.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index b36d5d0..6cee612 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -125,25 +125,38 @@ When passing `openid` to the scope and `organization` to the authorize params, y ### Validating Organizations when using Organization Login Prompt -When Organization login prompt is enabled on your application, but you haven't specified an Organization for the application's authorization endpoint, the `org_id` claim will be present on the ID token, and should be validated to ensure that the value received is expected or known. +When Organization login prompt is enabled on your application, but you haven't specified an Organization for the application's authorization endpoint, `org_id` or `org_name` claims will be present on the ID and access tokens, and should be validated to ensure that the value received is expected or known. Normally, validating the issuer would be enough to ensure that the token was issued by Auth0, and this check is performed by the SDK. However, in the case of organizations, additional checks should be made so that the organization within an Auth0 tenant is expected. -In particular, the `org_id` claim should be checked to ensure it is a value that is already known to the application. This could be validated against a known list of organization IDs, or perhaps checked in conjunction with the current request URL. e.g. the sub-domain may hint at what organization should be used to validate the ID Token. +In particular, the `org_id` and `org_name` claims should be checked to ensure it is a value that is already known to the application. This could be validated against a known list of organization IDs, or perhaps checked in conjunction with the current request URL. e.g. the sub-domain may hint at what organization should be used to validate the ID Token. For `org_id`, this should be a **case-sensitive, exact match check**. For `org_name`, this should be a **case-insentive check**. + +The decision to validate the `org_id` or `org_name` claim is determined by the expected organization ID or name having an `org_` prefix. Here is an example using it in your `callback` method ```ruby - def callback - claims = request.env['omniauth.auth']['extra']['raw_info'] +def callback + claims = request.env['omniauth.auth']['extra']['raw_info'] + + validate_as_id = expected_org.start_with?('org_') - if claims["org"] && claims["org"] !== expected_org + if validate_as_id + if claims["org_id"] && claims["org_id"] !== expected_org + redirect_to '/unauthorized', status: 401 + else + session[:userinfo] = claims + redirect_to '/dashboard' + end + else + if claims["org_name"] && claims["org_name"].downcase !== expected_org.downcase redirect_to '/unauthorized', status: 401 else session[:userinfo] = claims redirect_to '/dashboard' end end +end ``` For more information, please read [Work with Tokens and Organizations](https://auth0.com/docs/organizations/using-tokens) on Auth0 Docs. From b902386213e6e665c1884bbcd9642eb0b692949d Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Thu, 13 Jul 2023 12:17:30 +0000 Subject: [PATCH 4/5] chore: remove focus filter on tests --- spec/omniauth/auth0/jwt_validator_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/omniauth/auth0/jwt_validator_spec.rb b/spec/omniauth/auth0/jwt_validator_spec.rb index 7fe6273..3f64d3b 100644 --- a/spec/omniauth/auth0/jwt_validator_spec.rb +++ b/spec/omniauth/auth0/jwt_validator_spec.rb @@ -476,7 +476,7 @@ expect(id_token['auth_time']).to eq(auth_time) end - context 'Organization claim validation', focus: true do + context 'Organization claim validation' do it 'should fail when authorize params has organization but org_id is missing in the token' do payload = { iss: "https://#{domain}/", From d74110686f5e8d39819fe1bc8357a2c270715e6c Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Thu, 13 Jul 2023 15:59:31 +0000 Subject: [PATCH 5/5] chore: added another test case for org name mismatch --- spec/omniauth/auth0/jwt_validator_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/omniauth/auth0/jwt_validator_spec.rb b/spec/omniauth/auth0/jwt_validator_spec.rb index 3f64d3b..eb9426b 100644 --- a/spec/omniauth/auth0/jwt_validator_spec.rb +++ b/spec/omniauth/auth0/jwt_validator_spec.rb @@ -529,6 +529,24 @@ }))) end + it 'should fail when authorize params has organization but token org_name does not match' do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_name: 'another-organization' + } + + token = make_hs256_token(payload) + expect do + jwt_validator.verify(token, { organization: 'my-organization' }) + end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({ + message: "Organization Name (org_name) claim value mismatch in the ID token; expected 'my-organization', found 'another-organization'" + }))) + end + it 'should not fail when correctly given an organization ID' do payload = { iss: "https://#{domain}/",