Skip to content

Commit 6f8262d

Browse files
[SDK-4410] Support Organization Name in JWT validation (#184)
1 parent 708bc2e commit 6f8262d

File tree

4 files changed

+147
-40
lines changed

4 files changed

+147
-40
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Ruby",
3-
"image": "mcr.microsoft.com/devcontainers/ruby:3.1",
3+
"image": "mcr.microsoft.com/devcontainers/ruby:3.2",
44
"features": {
55
"ghcr.io/devcontainers/features/node:1": {
66
"version": "lts"

EXAMPLES.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,38 @@ When passing `openid` to the scope and `organization` to the authorize params, y
125125
126126
### Validating Organizations when using Organization Login Prompt
127127
128-
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.
128+
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.
129129
130130
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.
131131
132-
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.
132+
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**.
133+
134+
The decision to validate the `org_id` or `org_name` claim is determined by the expected organization ID or name having an `org_` prefix.
133135
134136
Here is an example using it in your `callback` method
135137
136138
```ruby
137-
def callback
138-
claims = request.env['omniauth.auth']['extra']['raw_info']
139+
def callback
140+
claims = request.env['omniauth.auth']['extra']['raw_info']
141+
142+
validate_as_id = expected_org.start_with?('org_')
139143
140-
if claims["org"] && claims["org"] !== expected_org
144+
if validate_as_id
145+
if claims["org_id"] && claims["org_id"] !== expected_org
146+
redirect_to '/unauthorized', status: 401
147+
else
148+
session[:userinfo] = claims
149+
redirect_to '/dashboard'
150+
end
151+
else
152+
if claims["org_name"] && claims["org_name"].downcase !== expected_org.downcase
141153
redirect_to '/unauthorized', status: 401
142154
else
143155
session[:userinfo] = claims
144156
redirect_to '/dashboard'
145157
end
146158
end
159+
end
147160
```
148161
149162
For more information, please read [Work with Tokens and Organizations](https://auth0.com/docs/organizations/using-tokens) on Auth0 Docs.

lib/omniauth/auth0/jwt_validator.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
module OmniAuth
88
module Auth0
99
# JWT Validator class
10+
# rubocop:disable Metrics/
1011
class JWTValidator
1112
attr_accessor :issuer, :domain
1213

@@ -264,12 +265,27 @@ def verify_auth_time(id_token, leeway, max_age)
264265
end
265266

266267
def verify_org(id_token, organization)
267-
if organization
268+
return unless organization
269+
270+
validate_as_id = organization.start_with? 'org_'
271+
272+
if validate_as_id
268273
org_id = id_token['org_id']
269274
if !org_id || !org_id.is_a?(String)
270-
raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim must be a string present in the ID token")
275+
raise OmniAuth::Auth0::TokenValidationError,
276+
'Organization Id (org_id) claim must be a string present in the ID token'
271277
elsif org_id != organization
272-
raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'")
278+
raise OmniAuth::Auth0::TokenValidationError,
279+
"Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'"
280+
end
281+
else
282+
org_name = id_token['org_name']
283+
if !org_name || !org_name.is_a?(String)
284+
raise OmniAuth::Auth0::TokenValidationError,
285+
'Organization Name (org_name) claim must be a string present in the ID token'
286+
elsif org_name.downcase != organization.downcase
287+
raise OmniAuth::Auth0::TokenValidationError,
288+
"Organization Name (org_name) claim value mismatch in the ID token; expected '#{organization}', found '#{org_name}'"
273289
end
274290
end
275291
end

spec/omniauth/auth0/jwt_validator_spec.rb

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -476,41 +476,119 @@
476476
expect(id_token['auth_time']).to eq(auth_time)
477477
end
478478

479-
it 'should fail when authorize params has organization but org_id is missing in the token' do
480-
payload = {
481-
iss: "https://#{domain}/",
482-
sub: 'sub',
483-
aud: client_id,
484-
exp: future_timecode,
485-
iat: past_timecode
486-
}
479+
context 'Organization claim validation' do
480+
it 'should fail when authorize params has organization but org_id is missing in the token' do
481+
payload = {
482+
iss: "https://#{domain}/",
483+
sub: 'sub',
484+
aud: client_id,
485+
exp: future_timecode,
486+
iat: past_timecode
487+
}
487488

488-
token = make_hs256_token(payload)
489-
expect do
490-
jwt_validator.verify(token, { organization: 'Test Org' })
491-
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
492-
message: "Organization Id (org_id) claim must be a string present in the ID token"
493-
}))
494-
end
489+
token = make_hs256_token(payload)
490+
expect do
491+
jwt_validator.verify(token, { organization: 'org_123' })
492+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
493+
message: "Organization Id (org_id) claim must be a string present in the ID token"
494+
}))
495+
end
495496

496-
it 'should fail when authorize params has organization but token org_id does not match' do
497-
payload = {
498-
iss: "https://#{domain}/",
499-
sub: 'sub',
500-
aud: client_id,
501-
exp: future_timecode,
502-
iat: past_timecode,
503-
org_id: 'Wrong Org'
504-
}
497+
it 'should fail when authorize params has organization but org_name is missing in the token' do
498+
payload = {
499+
iss: "https://#{domain}/",
500+
sub: 'sub',
501+
aud: client_id,
502+
exp: future_timecode,
503+
iat: past_timecode
504+
}
505505

506-
token = make_hs256_token(payload)
507-
expect do
508-
jwt_validator.verify(token, { organization: 'Test Org' })
509-
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
510-
message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'Test Org', found 'Wrong Org'"
511-
}))
512-
end
506+
token = make_hs256_token(payload)
507+
expect do
508+
jwt_validator.verify(token, { organization: 'my-organization' })
509+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({
510+
message: 'Organization Name (org_name) claim must be a string present in the ID token'
511+
})))
512+
end
513513

514+
it 'should fail when authorize params has organization but token org_id does not match' do
515+
payload = {
516+
iss: "https://#{domain}/",
517+
sub: 'sub',
518+
aud: client_id,
519+
exp: future_timecode,
520+
iat: past_timecode,
521+
org_id: 'org_5678'
522+
}
523+
524+
token = make_hs256_token(payload)
525+
expect do
526+
jwt_validator.verify(token, { organization: 'org_1234' })
527+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({
528+
message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'org_1234', found 'org_5678'"
529+
})))
530+
end
531+
532+
it 'should fail when authorize params has organization but token org_name does not match' do
533+
payload = {
534+
iss: "https://#{domain}/",
535+
sub: 'sub',
536+
aud: client_id,
537+
exp: future_timecode,
538+
iat: past_timecode,
539+
org_name: 'another-organization'
540+
}
541+
542+
token = make_hs256_token(payload)
543+
expect do
544+
jwt_validator.verify(token, { organization: 'my-organization' })
545+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and(having_attributes({
546+
message: "Organization Name (org_name) claim value mismatch in the ID token; expected 'my-organization', found 'another-organization'"
547+
})))
548+
end
549+
550+
it 'should not fail when correctly given an organization ID' do
551+
payload = {
552+
iss: "https://#{domain}/",
553+
sub: 'sub',
554+
aud: client_id,
555+
exp: future_timecode,
556+
iat: past_timecode,
557+
org_id: 'org_1234'
558+
}
559+
560+
token = make_hs256_token(payload)
561+
jwt_validator.verify(token, { organization: 'org_1234' })
562+
end
563+
564+
it 'should not fail when correctly given an organization name' do
565+
payload = {
566+
iss: "https://#{domain}/",
567+
sub: 'sub',
568+
aud: client_id,
569+
exp: future_timecode,
570+
iat: past_timecode,
571+
org_name: 'my-organization'
572+
}
573+
574+
token = make_hs256_token(payload)
575+
jwt_validator.verify(token, { organization: 'my-organization' })
576+
end
577+
578+
it 'should not fail when given an organization name in a different casing' do
579+
payload = {
580+
iss: "https://#{domain}/",
581+
sub: 'sub',
582+
aud: client_id,
583+
exp: future_timecode,
584+
iat: past_timecode,
585+
org_name: 'MY-ORGANIZATION'
586+
}
587+
588+
token = make_hs256_token(payload)
589+
jwt_validator.verify(token, { organization: 'my-organization' })
590+
end
591+
end
514592
it 'should fail for RS256 token when kid is incorrect' do
515593
domain = 'example.org'
516594
sub = 'abc123'

0 commit comments

Comments
 (0)