From 43030b5f8e059e4bba848465bfe9e2c836e9f39f Mon Sep 17 00:00:00 2001 From: David Patrick Date: Thu, 1 Apr 2021 10:48:52 -0700 Subject: [PATCH] Org Support (#124) --- README.md | 69 +++++++++++++++++++++++ lib/omniauth/auth0/jwt_validator.rb | 13 +++++ lib/omniauth/strategies/auth0.rb | 2 +- spec/omniauth/auth0/jwt_validator_spec.rb | 35 ++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab2daeb..1141341 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,78 @@ In some scenarios, you may need to pass specific query parameters to `/authorize - `connection_scope` - `prompt` - `screen_hint` (only relevant to New Universal Login Experience) +- `organization` +- `invitation` Simply pass these query parameters to your OmniAuth redirect endpoint to enable their behavior. +## Examples + +### Auth0 Organizations (Closed Beta) + +Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. + +Using Organizations, you can: + +- Represent teams, business customers, partner companies, or any logical grouping of users that should have different ways of accessing your applications, as organizations. +- Manage their membership in a variety of ways, including user invitation. +- Configure branded, federated login flows for each organization. +- Implement role-based access control, such that users can have different roles when authenticating in the context of different organizations. +- Build administration capabilities into your products, using Organizations APIs, so that those businesses can manage their own organizations. + +Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. + +#### Logging in with an Organization + +Logging in with an Organization is as easy as passing the parameters to the authorize endpoint. You can do this with + +```ruby +<%= + button_to 'Login', 'auth/auth0', + method: :post, + params: { + # Found in your Auth0 dashboard, under Organization settings: + organization: '{AUTH0_ORGANIZATION}' + } +%> +``` + +Alternatively you can configure the organization when you register the provider: + +```ruby +provider + :auth0, + ENV['AUTH0_CLIENT_ID'], + ENV['AUTH0_CLIENT_SECRET'], + ENV['AUTH0_DOMAIN'], + { + authorize_params: { + scope: 'openid read:users', + audience: 'https://{AUTH0_DOMAIN}/api', + organization: '{AUTH0_ORGANIZATION}' + } + } +``` + +#### Accepting user invitations + +Auth0 Organizations allow users to be invited using emailed links, which will direct a user back to your application. The URL the user will arrive at is based on your configured `Application Login URI`, which you can change from your Application's settings inside the Auth0 dashboard. + +When the user arrives at your application using an invite link, you can expect three query parameters to be provided: `invitation`, `organization`, and `organization_name`. These will always be delivered using a GET request. + +You can then supply those parametrs to a `button_to` or `link_to` helper + +```ruby +<%= + button_to 'Login', 'auth/auth0', + method: :post, + params: { + organization: '{YOUR_ORGANIZATION_ID}', + invitation: '{INVITE_CODE}' + } +%> +``` + ## Contribution We appreciate feedback and contribution to this repo! Before you get started, please see the following: diff --git a/lib/omniauth/auth0/jwt_validator.rb b/lib/omniauth/auth0/jwt_validator.rb index e833c50..4484734 100644 --- a/lib/omniauth/auth0/jwt_validator.rb +++ b/lib/omniauth/auth0/jwt_validator.rb @@ -174,6 +174,7 @@ def verify_claims(id_token, authorize_params) leeway = authorize_params[:leeway] || 60 max_age = authorize_params[:max_age] nonce = authorize_params[:nonce] + organization = authorize_params[:organization] verify_iss(id_token) verify_sub(id_token) @@ -183,6 +184,7 @@ def verify_claims(id_token, authorize_params) verify_nonce(id_token, nonce) verify_azp(id_token) verify_auth_time(id_token, leeway, max_age) + verify_org(id_token, organization) end def verify_iss(id_token) @@ -260,6 +262,17 @@ def verify_auth_time(id_token, leeway, max_age) end end end + + def verify_org(id_token, organization) + if organization + 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") + 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}'") + end + end + end end end end diff --git a/lib/omniauth/strategies/auth0.rb b/lib/omniauth/strategies/auth0.rb index 694cddd..eac4b08 100644 --- a/lib/omniauth/strategies/auth0.rb +++ b/lib/omniauth/strategies/auth0.rb @@ -84,7 +84,7 @@ def client # Define the parameters used for the /authorize endpoint def authorize_params params = super - %w[connection connection_scope prompt screen_hint].each do |key| + %w[connection connection_scope prompt screen_hint organization invitation].each do |key| params[key] = request.params[key] if request.params.key?(key) end diff --git a/spec/omniauth/auth0/jwt_validator_spec.rb b/spec/omniauth/auth0/jwt_validator_spec.rb index de6ea40..239d7ca 100644 --- a/spec/omniauth/auth0/jwt_validator_spec.rb +++ b/spec/omniauth/auth0/jwt_validator_spec.rb @@ -476,6 +476,41 @@ 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', focus: true 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 + + it 'should fail when authorize params has organization but token org_id does not match', focus: true do + payload = { + iss: "https://#{domain}/", + sub: 'sub', + aud: client_id, + exp: future_timecode, + iat: past_timecode, + org_id: 'Wrong Org' + } + + 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 + it 'should fail for RS256 token when kid is incorrect' do domain = 'example.org' sub = 'abc123'