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

[SDK-4410] Support Organization Name in JWT validation #184

Merged
merged 5 commits into from
Jul 14, 2023
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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
23 changes: 18 additions & 5 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 19 additions & 3 deletions lib/omniauth/auth0/jwt_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
module OmniAuth
module Auth0
# JWT Validator class
# rubocop:disable Metrics/
class JWTValidator
attr_accessor :issuer, :domain

Expand Down Expand Up @@ -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
Expand Down
140 changes: 109 additions & 31 deletions spec/omniauth/auth0/jwt_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -476,41 +476,119 @@
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' 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an equivalent test for org_name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, thanks for catching that. Fixed in d741106

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 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}/",
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'
Expand Down