Skip to content

Commit

Permalink
[SDK-4386] Support Organization Name in Authorize (#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevehobbsdev authored Jul 13, 2023
1 parent 948c1f3 commit 438e1b7
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ruby/.devcontainer/base.Dockerfile

# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster
ARG VARIANT="3.1-bullseye"
ARG VARIANT="3.2-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/ruby:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
Expand Down
18 changes: 10 additions & 8 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Note that Organizations is currently only available to customers on our Enterpri

### Logging in with an Organization

Configure the Authentication API client and pass your Organization ID to the authorize url:
Configure the Authentication API client and pass your Organization ID or name to the authorize url:

```ruby
require 'auth0'
Expand All @@ -94,7 +94,7 @@ require 'auth0'
client_id: '{YOUR_APPLICATION_CLIENT_ID}',
client_secret: '{YOUR_APPLICATION_CLIENT_SECRET}',
domain: '{YOUR_TENANT}.auth0.com',
organization: "{YOUR_ORGANIZATION_ID}"
organization: "{YOUR_ORGANIZATION_ID_OR_NAME}"
)

universal_login_url = @auth0_client.authorization_url("https://{YOUR_APPLICATION_CALLBACK_URL}")
Expand All @@ -113,7 +113,7 @@ require 'auth0'
client_id: '{YOUR_APPLICATION_CLIENT_ID}',
client_secret: '{YOUR_APPLICATION_CLIENT_ID}',
domain: '{YOUR_TENANT}.auth0.com',
organization: "{YOUR_ORGANIZATION_ID}"
organization: "{YOUR_ORGANIZATION_ID_OR_NAME}"
)

universal_login_url = @auth0_client.authorization_url("https://{YOUR_APPLICATION_CALLBACK_URL}", {
Expand Down Expand Up @@ -148,7 +148,7 @@ The method takes the following optional keyword parameters:
| `max_age` | Integer | The `max_age` value you sent in the call to `/authorize`, if any. | `nil` |
| `issuer` | String | By default the `iss` claim will be checked against the URL of your **Auth0 Domain**. Use this parameter to override that. | `nil` |
| `audience` | String | By default the `aud` claim will be compared to your **Auth0 Client ID**. Use this parameter to override that. | `nil` |
| `organization` | String | By default the `org_id` claim will be compared to your **Organization ID**. Use this parameter to override that. | `nil` |
| `organization` | String | By default the `org_id` or `org_name` claims will be compared to the `organization` value specified at client creation. Use this parameter to override that. | `nil` |

You can check the signing algorithm value under **Advanced Settings > OAuth > JsonWebToken Signature Algorithm** in your Auth0 application settings panel. [We recommend](https://auth0.com/docs/tokens/concepts/signing-algorithms#our-recommendation) that you make use of asymmetric signing algorithms like `RS256` instead of symmetric ones like `HS256`.

Expand All @@ -170,23 +170,25 @@ rescue Auth0::InvalidIdToken => e
end
```

### Organization ID Token Validation
### Organization claim validation

If an org_id claim is present in the Access Token, then the claim should be validated by the API to ensure that the value received is expected or known.
If an `org_id` or `org_name` claim is present in the access token, then the claim should be validated by the API to ensure that the value received is expected or known.

In particular:

- The issuer (iss) claim should be checked to ensure the token was issued by Auth0

- 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 Access Token.
- the `org_id` or `org_name` claim should be checked to ensure it is a value that is already known to the application. Which claim you check depends on the organization value being validated: if it starts with `org_`, validate against the `org_id` claim. Otherwise, validate against `org_name`. Further, `org_name` validation should be done using a **case-insensitive** check, whereas `org_id` should be an exact case-sensitive match.

This could be validated against a known list of organization IDs or names, 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 Access Token.

Normally, validating the issuer would be enough to ensure that the token was issued by Auth0. In the case of organizations, additional checks should be made so that the organization within an Auth0 tenant is expected.

If the claim cannot be validated, then the application should deem the token invalid.

```ruby
begin
@auth0_client.validate_id_token 'YOUR_ID_TOKEN', organization: '{Expected org_id}'
@auth0_client.validate_id_token 'YOUR_ID_TOKEN', organization: '{Expected org_id or org_name}'
rescue Auth0::InvalidIdToken => e
# In this case the ID Token contents should not be trusted
end
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PATH
specs:
auth0 (5.13.0)
addressable (~> 2.8)
jwt (~> 2.5)
jwt (~> 2.7)
rest-client (~> 2.1)
retryable (~> 3.0)
zache (~> 0.12)
Expand Down
2 changes: 1 addition & 1 deletion auth0.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.require_paths = ['lib']

s.add_runtime_dependency 'rest-client', '~> 2.1'
s.add_runtime_dependency 'jwt', '~> 2.5'
s.add_runtime_dependency 'jwt', '~> 2.7'
s.add_runtime_dependency 'zache', '~> 0.12'
s.add_runtime_dependency 'addressable', '~> 2.8'
s.add_runtime_dependency 'retryable', '~> 3.0'
Expand Down
25 changes: 19 additions & 6 deletions lib/auth0/mixins/validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,26 @@ def validate_nonce(claims, expected)
end

def validate_org(claims, expected)
unless claims.key?('org_id') && claims['org_id'].is_a?(String)
raise Auth0::InvalidIdToken, 'Organization Id (org_id) claim must be a string present in the ID token'
end
validate_as_id = expected.start_with? 'org_'

if validate_as_id
unless claims.key?('org_id') && claims['org_id'].is_a?(String)
raise Auth0::InvalidIdToken, 'Organization Id (org_id) claim must be a string present in the ID token'
end

unless expected == claims['org_id']
raise Auth0::InvalidIdToken, "Organization Id (org_id) claim value mismatch in the ID token; expected \"#{expected}\","\
" found \"#{claims['org_id']}\""
unless expected == claims['org_id']
raise Auth0::InvalidIdToken, "Organization Id (org_id) claim value mismatch in the ID token; expected \"#{expected}\","\
" found \"#{claims['org_id']}\""
end
else
unless claims.key?('org_name') && claims['org_name'].is_a?(String)
raise Auth0::InvalidIdToken, 'Organization Name (org_name) claim must be a string present in the ID token'
end

unless expected.downcase == claims['org_name'].downcase
raise Auth0::InvalidIdToken, "Organization Name (org_name) claim value mismatch in the ID token; expected \"#{expected}\","\
" found \"#{claims['org_name']}\""
end
end
end

Expand Down
86 changes: 68 additions & 18 deletions spec/lib/auth0/mixins/validation_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# rubocop:disable Metrics/BlockLength
require 'spec_helper'
require 'jwt'

RSA_PUB_KEY_JWK_1 = { 'kty': "RSA", 'use': 'sig', 'n': "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", 'e': 'AQAB', 'kid': 'test-key-1' }.freeze
RSA_PUB_KEY_JWK_2 = { 'kty': "RSA", 'use': 'sig', 'n': "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", 'e': 'AQAB', 'kid': 'test-key-2' }.freeze
Expand All @@ -13,8 +14,14 @@
CLOCK = 1587592561 # Apr 22 2020 21:56:01 UTC
CONTEXT = { algorithm: Auth0::Algorithm::HS256.secret(HMAC_SHARED_SECRET), leeway: LEEWAY, audience: 'tokens-test-123', issuer: 'https://tokens-test.auth0.com/', clock: CLOCK }.freeze

def build_id_token(payload = {})
default_payload = { iss: CONTEXT[:issuer], sub: 'user123', aud: CONTEXT[:audience], exp: CLOCK, iat: CLOCK }
JWT.encode(default_payload.merge(payload), HMAC_SHARED_SECRET, 'HS256')
end

describe Auth0::Mixins::Validation::IdTokenValidator do
subject { @instance }
let (:minimal_id_token) { build_id_token }

context 'instance' do
it 'is expected respond to :validate' do
Expand Down Expand Up @@ -285,30 +292,73 @@
expect { instance.validate(token) }.to raise_exception("Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time \"#{clock}\" is after last auth at \"#{auth_time}\"")
end

it 'is expected not to raise an error when org_id exsist in the token, but not required' do
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
expect { @instance.validate(token) }.not_to raise_exception
end
context 'Organization claims validation' do
it 'is expected not to raise an error when org_id exsist in the token, but not required' do
token = build_id_token org_id: 'org_123'
expect { @instance.validate(token) }.not_to raise_exception
end

it 'is expected to raise an error with a missing but required organization' do
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE4MTg1LCJpYXQiOjE2MTY0NDUzODUsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTc4NX0.UMo5pmgceXO9lIKzbk7X0ZhE5DOe0IP2LfMKdUj03zQ'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'a1b2c3d4e5' }))
it 'is expected not to raise an error when org_name exists in the token, but not required' do
token = build_id_token org_name: 'my-organization'
expect { @instance.validate(token) }.not_to raise_exception
end

expect { instance.validate(token) }.to raise_exception('Organization Id (org_id) claim must be a string present in the ID token')
end
it 'is expected to raise an error with a missing but required organization ID' do
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'org_1234' }))
expect { instance.validate(minimal_id_token) }.to raise_exception('Organization Id (org_id) claim must be a string present in the ID token')
end

it 'is expected to raise an error with an invalid organization' do
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'a1b2c3d4e5' }))
it 'is expected to raise an error with a missing but required organization name' do
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'my-organization' }))
expect { instance.validate(minimal_id_token) }.to raise_exception('Organization Name (org_name) claim must be a string present in the ID token')
end

expect { instance.validate(token) }.to raise_exception('Organization Id (org_id) claim value mismatch in the ID token; expected "a1b2c3d4e5", found "testOrg"')
end
it 'is expected to raise an error with an invalid organization ID' do
token = build_id_token org_id: 'org_1234'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'org_5678' }))

expect { instance.validate(token) }.to raise_exception('Organization Id (org_id) claim value mismatch in the ID token; expected "org_5678", found "org_1234"')
end

it 'is expected to raise an error with an invalid organization name' do
token = build_id_token org_name: 'another-organization'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'my-organization' }))

expect { instance.validate(token) }.to raise_exception('Organization Name (org_name) claim value mismatch in the ID token; expected "my-organization", found "another-organization"')
end

it 'is expected to NOT raise an error with a valid organization ID' do
token = build_id_token org_id: 'org_1234'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'org_1234' }))

expect { instance.validate(token) }.not_to raise_exception
end

it 'is expected to NOT raise an error with a valid organization name' do
token = build_id_token org_name: 'my-organization'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'my-organization' }))

expect { instance.validate(token) }.not_to raise_exception
end

it 'is expected to NOT raise an error with organization name in different casing' do
token = build_id_token org_name: 'MY-ORGANIZATION'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'my-organization' }))

expect { instance.validate(token) }.not_to raise_exception
end

it 'is expected to NOT raise an error with a valid organization' do
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'testOrg' }))
it 'validates org_id when both claims are present in the token' do
token = build_id_token org_name: 'my-organization', org_id: 'org_1234'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'org_1234' }))
expect { instance.validate(token) }.not_to raise_exception
end

expect { instance.validate(token) }.not_to raise_exception
it 'validates org_name when both claims are present in the token' do
token = build_id_token org_name: 'my-organization', org_id: 'org_1234'
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'my-organization' }))
expect { instance.validate(token) }.not_to raise_exception
end
end
end
end
Expand Down

0 comments on commit 438e1b7

Please sign in to comment.